# AI-Rulez > The standards-compliant lifecycle tool for agent knowledge and capabilities: author, generate, bundle, validate, govern, publish ## Home Source: https://goldziher.github.io/ai-rulez/

AI-Rulez

AI-Rulez is a standards-compliant lifecycle tool for agent knowledge and capabilities. Keep your rules, context, skills, agents, commands, hooks, permissions and MCP servers in one source of truth, `.ai-rulez/`, and take them through every stage: ```text author -> generate -> bundle -> lint / validate -> govern -> publish ``` The claim is "standards compliant": each format ai-rulez names is checked against that standard's own schema or rules. [Standards](standards.md) lists the pinned spec versions, the conformance tests and the known gaps. #### The lifecycle | Section | What you do | Start with | | ------- | ----------- | ---------- | | **Author** | Write rules, context, skills, agents and checks as markdown; organize them with domains, profiles, roles and includes. `.ai-rulez/` is an [OKF](okf.md) bundle: OKF is the internal format, and `init`, `add` and the MCP tools write it. A pre-OKF tree still loads, with a deprecation notice; `ai-rulez migrate okf` converts it. | [Configuration](configuration.md), [Rules](rules.md), [Skill frontmatter](skills.md) | | **Generate** | Render native files for 52 harnesses, per project or per user, with hooks and permissions translated for each. | [Supported harnesses](harnesses.md), [AGENTS.md](agents-md.md), [User-level configuration](user-scope.md) | | **Bundle and publish** | Package plugin bundles, Agent Plugins and ARD manifests, then release them to GitHub, npm, OCI and marketplaces. | [Authoring plugins](plugins.md), [Agent Plugins](agent-plugins.md), [ARD](ard.md), [Publish](publish.md) | | **Validate** | Content and security checks with stable `AR` codes, deterministic verifiers and the per-standard validators. | [Strict validation](strict-validation.md), [Verifiers](verifiers.md) | | **Govern** | Pin content in a lock, record reviewer approvals, sign with Sigstore, set an organization policy and ship an SBOM. | [Trust model](trust-model.md), [Lock file](lockfile.md), [Signing](signing.md), [SBOM](sbom.md) | | **Operate** | Serve the configuration and skills over MCP, export telemetry, run evals and improve skills. | [MCP server](mcp-server.md), [Telemetry](telemetry.md), [Evals](evals.md) | #### Get started 1. [Install](installation.md) ai-rulez (`npx ai-rulez@latest` needs no install). 2. Follow the [Quick Start](quick-start.md): `ai-rulez init`, pick presets, `ai-rulez generate`. 3. Run `ai-rulez validate --strict`, then commit `.ai-rulez/` together with the generated files. ```bash ai-rulez init "my-project" ai-rulez generate ai-rulez validate --strict ``` Coming from 4.x? Run `ai-rulez migrate v5 --dry-run` and read [Migrating to v5](migration-v5.md). #### Project layout ```text project-root/ ├── .ai-rulez/ │ ├── config.toml # Presets, profiles, lifecycle settings │ ├── rules/ # Mandatory constraints │ ├── context/ # Reference documentation │ ├── skills/ # Skills (SKILL.md) │ ├── agents/, commands/ # Subagents and slash commands │ ├── checks/ # Code-review guidelines │ └── domains/ # Team or subsystem content ├── CLAUDE.md # Generated for Claude (rules go to .claude/rules/) ├── .cursor/rules/ # Generated for Cursor ├── GEMINI.md # Generated for Gemini └── .github/copilot-instructions.md ``` Generated files are committed by default. Set `gitignore = true` to keep them out of git through a managed `.gitignore` block instead. #### Reference - [CLI commands](cli.md): every command and flag; `ai-rulez --help` and `ai-rulez --help` are authoritative. - [Schema](schema.md): the JSON schemas of the configuration and of every `--format json` document. - [Embedding (Go API)](embedding.md): use ai-rulez as a library. - [Changelog](CHANGELOG.md). #### Getting help - **CLI help**: `ai-rulez --help`, `ai-rulez generate --help`, and so on. - **Rule explanations**: `ai-rulez validate --explain AR001`. - **Diagnostics**: `ai-rulez doctor` reports drift, removed presets and missing tools without changing anything. - **Issues**: report problems on [GitHub](https://github.com/Goldziher/ai-rulez/issues). This documentation covers **ai-rulez v5** (`version = "5.0"` in `config.toml`). ## Installation Source: https://goldziher.github.io/ai-rulez/installation/ Install `ai-rulez` using your preferred package manager. #### Package Managers === "Homebrew (macOS/Linux)" ```bash brew install goldziher/tap/ai-rulez ``` === "npm" ```bash npm install -g ai-rulez ``` === "pip" ```bash pip install ai-rulez ``` === "uv tool" ```bash uv tool install ai-rulez ``` !!! note "Go" The module path is `github.com/Goldziher/ai-rulez/v5`, and the binary is the `cmd/ai-rulez` package. Install it with the `/v5` path (a path without it resolves to an old 1.x build): ```bash go install github.com/Goldziher/ai-rulez/v5/cmd/ai-rulez@latest ``` Or build from a clone: ```bash git clone https://github.com/Goldziher/ai-rulez cd ai-rulez go build -o ai-rulez ./cmd/ai-rulez ``` #### Run Without Installing You can also run `ai-rulez` directly without a permanent installation. === "Python" ```bash uvx ai-rulez --help ``` === "Node.js" ```bash npx ai-rulez@latest --help ``` #### Shell Completion (Recommended) Enable tab completion for your shell to see all available commands and flags interactively. !!! tip "Highly Recommended" Setting up shell completion is a one-time step that makes the CLI much faster and easier to use. You'll be able to discover all commands just by pressing the `` key. === "Bash" Add to `~/.bashrc` or `~/.bash_profile`: ```bash source <(ai-rulez completion bash) ``` === "Zsh" Add to `~/.zshrc`: ```bash source <(ai-rulez completion zsh) ``` === "Fish" Add to `~/.config/fish/config.fish`: ```bash ai-rulez completion fish | source ``` === "PowerShell" Add to your PowerShell profile: ```powershell ai-rulez completion powershell | Out-String | Invoke-Expression ``` #### Verify Installation Check that the installation was successful: ```bash ai-rulez version ``` --- #### Next Steps - **[Quick Start Guide](quick-start.md)**: Get up and running in minutes. ## Quick Start Source: https://goldziher.github.io/ai-rulez/quick-start/ Get AI-Rulez running in 5 minutes. This page covers the first two lifecycle stages, author and generate, then validates the result; [Next steps](#next-steps) points to bundling, publishing and governance. #### Step 1: Initialize Your Project Create a new configuration: ```bash ai-rulez init "my-project" ``` This creates a `.ai-rulez/` directory with: ```text .ai-rulez/ ├── config.toml ├── rules/ │ └── code-quality.md ├── context/ │ └── architecture.md ├── skills/ │ ├── code-reviewer/ │ │ └── SKILL.md │ └── ai-rulez/ │ └── SKILL.md └── agents/ ``` #### Step 2: Configure Your Presets Edit `.ai-rulez/config.toml` to specify which tools to generate for: ```toml version = "5.0" name = "my-project" description = "My awesome project" # Tools to generate configuration for presets = ["claude", "cursor", "gemini"] # Default profile when none specified default = "full" # Named profiles for different team needs [profiles] full = [] # Empty = root content only ``` #### Step 3: Add Your Rules Create rule files in `.ai-rulez/rules/`: **`.ai-rulez/rules/code-quality.md`:** ```markdown --- priority: high --- # Code Quality - Use meaningful variable names - Comment complex logic - All tests must pass before merge ``` **`.ai-rulez/rules/git-workflow.md`:** ```markdown --- priority: medium --- # Git Workflow 1. Feature branches from main 2. Squash commits before merge 3. Require code review before merge ``` #### Step 4: Add Context Documentation Create context files in `.ai-rulez/context/`: **`.ai-rulez/context/architecture.md`:** ```markdown # Architecture ## System Design - 3 microservices behind an API Gateway - PostgreSQL for persistence - Kubernetes for orchestration ## Stack - Backend: Go - Frontend: React - Infrastructure: Kubernetes ``` #### Step 5: Define Skills (Optional) Create specialized AI prompts in `.ai-rulez/skills/`: **`.ai-rulez/skills/code-reviewer/SKILL.md`:** ```markdown --- priority: high description: "Code reviewer for quality assurance" --- # Code Reviewer Review code for: - Quality and maintainability - Test coverage - Performance issues Responsibilities: 1. Review pull requests for correctness 2. Suggest improvements 3. Verify test coverage ``` #### Step 6: Generate Outputs Generate configuration files for all your tools: ```bash ai-rulez generate ``` This creates: - `AGENTS.md` and `.agents/skills/` (shared by the presets that read them; `agents_md` is on by default) - `CLAUDE.md`, which imports `@AGENTS.md`, and `.claude/rules/` (from claude preset; with the default split mode your path-scoped rules are in `.claude/rules/`) - `.cursor/rules/` (from cursor preset) - `.gemini/settings.json`, which points Gemini at `AGENTS.md` (from gemini preset) Set `agents_md = false` to get a separate file per tool (`GEMINI.md`, and so on) instead. #### Step 7: Verify and Commit Check that files were generated: ```bash ls -la AGENTS.md CLAUDE.md .agents/skills/ .gemini/settings.json ``` Commit the sources and the generated files: ```bash git add .ai-rulez/ AGENTS.md CLAUDE.md .agents/ .claude/ .cursor/ .gemini/ git commit -m "docs: initialize AI assistant configuration" ``` Generated files are not gitignored by default. To keep them out of git and let teammates run `ai-rulez generate` themselves, set `gitignore = true` in `config.toml` (or pass `generate --gitignore`); `generate` then maintains a managed block in `.gitignore`. Personal notes that should not be shared go in `.ai-rulez/local/` (`ai-rulez add rule my-notes --local`); see [Local Configuration](local-overrides.md). #### Multi-Team Setup For projects with multiple teams, add domains: **1. Create domain structure:** ```bash mkdir -p .ai-rulez/domains/backend/rules mkdir -p .ai-rulez/domains/frontend/rules ``` **2. Add domain-specific rules:** **`.ai-rulez/domains/backend/rules/database.md`:** ```markdown --- priority: critical --- # Database Standards - Use prepared statements - Add migrations for schema changes - Index foreign keys ``` **3. Update `config.toml`:** ```toml version = "5.0" name = "my-platform" presets = ["claude", "cursor"] default = "full" [profiles] full = ["backend", "frontend"] backend = ["backend"] frontend = ["frontend"] ``` **4. Generate for specific teams:** ```bash # Backend team gets root + backend content ai-rulez generate --profile backend # Frontend team gets root + frontend content ai-rulez generate --profile frontend # CI/QA gets everything ai-rulez generate --profile full ``` #### Common Tasks ##### Update Rules Edit any file in `.ai-rulez/rules/` and regenerate: ```bash # Edit a rule vim .ai-rulez/rules/code-quality.md # Regenerate outputs ai-rulez generate ``` ##### Add a New Domain ```bash mkdir -p .ai-rulez/domains/newdomain/rules mkdir -p .ai-rulez/domains/newdomain/context # Add rules and context files... # Update config.toml to reference the domain ``` ##### Change Tool Configuration Edit presets in `config.toml`: ```toml presets = ["claude", "cursor", "devin", "copilot"] ``` ##### Create Custom Output For tools not in the built-in list: ```toml [[presets]] name = "my-tool" type = "markdown" path = "docs/MY_TOOL.md" ``` #### Troubleshooting ##### Generated files aren't updating Make sure you ran `ai-rulez generate`: ```bash ai-rulez generate ``` ##### Content not appearing in output Check that your file is in the correct location: - Root content: `.ai-rulez/rules/`, `.ai-rulez/context/` - Domain content: `.ai-rulez/domains/{name}/rules/`, etc. ##### Validation fails Check your configuration: ```bash ai-rulez validate ``` This will show errors in your setup. #### Next Steps Continue along the lifecycle: - **Author**: [Configuration](configuration.md), [Domains & Profiles](domains.md), [Includes](includes.md) - **Generate**: [Supported harnesses](harnesses.md), [Custom presets](profiles.md), [User-level configuration](user-scope.md) - **Bundle and publish**: [Authoring plugins](plugins.md), [Publish](publish.md) - **Validate**: [Strict validation](strict-validation.md) - **Govern**: [Trust model](trust-model.md), [Lock file](lockfile.md) - **Operate**: [MCP server](mcp-server.md) - **Standards**: [which standards ai-rulez generates and validates](standards.md) ## Standards Source: https://goldziher.github.io/ai-rulez/standards/ ai-rulez claims conformance with a standard only when a passing test backs the claim. Every test named here is in [`tests/conformance`](https://github.com/Goldziher/ai-rulez/tree/main/tests/conformance) and runs with `go test ./...`. It validates a golden or freshly generated document against the standard's own schema, vendored and pinned under `tests/conformance/schemas`, with no network access. The README in that directory says how to refresh a schema. Status values: - **conformant**: our output validates against the standard's official machine-readable schema in a test. - **partial**: tested, but the standard has no official schema (the test transcribes its written rules), or only part of what we emit is covered. The gaps column says what is missing. - **planned**: implemented or intended, with no conformance test yet. | Standard | Pinned version | Status | What we generate and validate | Test | Known gaps | | --- | --- | --- | --- | --- | --- | | [Agent Plugins](https://agent-plugins.org) | 1.0.0 (published, default) and 1.1.0 (working draft); [schema repo](https://github.com/agentplugins/agent-plugins-spec) at `ff8ab5e` | conformant | `plugin.json`, `mcp.json` and `skills/` from `generate --plugin`; read back and checked by `validate` ([page](agent-plugins.md)) | `TestAgentPluginsGoldenConformsToTheOfficialSchemas`, `TestAgentPluginsGeneratedByTheCLIConformToTheOfficialSchemas` | The official name pattern uses a lookahead RE2 cannot compile; the test applies the same rule in Go. Extension namespaces are not schema-checked beyond the base manifest. | | [ARD](https://agenticresourcediscovery.org/) | 0.91 (proposal); [ard-spec](https://github.com/ards-project/ard-spec) at `b76f235` | conformant | `ard.json` from `publish --emit ard`; entry and manifest schema plus the URN grammar ([page](ard.md)) | `TestARDManifestGoldenConformsToTheEntrySchema`, `TestARDValidatorAcceptsTheGoldenAndNamesItsSpecVersion` | The spec is a proposal; media types and term names can change. The plugin entry type is an ai-rulez vendor type. The golden comes from the in-package model, not a release. | | [llms.txt](https://llmstxt.org/) | No versioned release; format as published at llmstxt.org | partial | `llms.txt` and `llms-full.txt` from the `llms-txt` preset, and for the docs site ([page](llms-txt.md)) | `TestGeneratedLLMSTxtFollowsTheFormat`, `TestPublishedDocsSiteLLMSTxtFollowsTheFormat` | No official schema. The test transcribes the format (H1, optional blockquote, H2 link lists). `llms-full.txt` is checked for its H1 only; the format does not define it. | | [Agent Skills](https://agentskills.io/specification) | No versioned release; specification as published at agentskills.io | partial | `SKILL.md` for every skill, with its resources, under `.claude/skills`, `.agents/skills` and `skills/` ([page](skills.md)) | `TestGeneratedSkillsFollowTheAgentSkillsSpecification`, plus the skill checks in the Agent Plugins tests | No official schema. The test transcribes the frontmatter rules (name, description, compatibility, license, metadata, allowed-tools). Resource layout is not checked. | | [AGENTS.md](https://agents.md) | No versioned release | partial | `AGENTS.md` at the project root ([page](agents-md.md)) | `TestGeneratedAgentsMDIsPlainMarkdownAtTheRoot` | The format has no schema and no required fields. The test checks that the file is plain Markdown at the root with the project's guidance. Nested `AGENTS.md` files in monorepos are not covered. | | MCP server configuration | The `mcp.json` schema of Agent Plugins 1.0.0 and 1.1.0 | partial | `mcp.json` (stdio, streamable-http and sse servers) | `TestMCPServerEntriesUseTheSchemaTransportTypes`, plus the `mcp.json` schema check in the Agent Plugins tests | The per-harness files (`.mcp.json`, `.codex/config.toml` and others) follow each tool's own format and are not validated against a schema here. | | MCP server card | None pinned | planned | Embedded as `application/mcp-server-card+json` entries in `ard.json` | None | No official card schema is vendored; the card shape is only checked as part of the ARD entry. | | [CycloneDX](https://cyclonedx.org/) | 1.6 | conformant | `ai-rulez sbom` ([page](sbom.md)) | `TestCycloneDXOutputConformsToTheOfficial16Schema` | The test validates structure. NTIA minimum-element and license-expression policy checks are not run. | | [SPDX](https://spdx.dev/) | 2.3 (JSON) | conformant | `ai-rulez sbom --type spdx-json` | `TestSPDXOutputConformsToTheOfficial23Schema` | Structure only. The in-package tests also check unique identifiers and resolving relationships. | | [in-toto Statement](https://github.com/in-toto/attestation) and [DSSE](https://github.com/secure-systems-lab/dsse) | Statement v1; DSSE v1 | partial | Signed statements inside Sigstore bundles from `ai-rulez sign`; `verify` checks them ([page](signing.md)) | `TestSignedStatementIsADSSEEnvelopeOverAnInTotoStatement` | Neither project publishes a JSON Schema, so the vendored schemas are hand-written from the specifications. Sigstore bundle structure and certificate chains are covered by `internal/signing` tests, not by a conformance schema. The `v0.1` statement type is accepted on verification only. | | OKF | v0.2 | not covered here | Native format ([page](okf.md)) | `internal/okf` | Tracked in [#281](https://github.com/Goldziher/ai-rulez/issues/281). | | OpenTelemetry (OTLP) | n/a | not covered here | Export only ([page](telemetry.md)) | n/a | No document we write is validated against an OTLP schema. | #### Reading a status A conformant status says the documents the tests produce validate. It does not say every possible configuration does: the tests use representative projects (rules, context, skills, stdio and remote MCP servers). A schema accepting a document also does not mean a given client will load it; the per-harness behaviour is in [harness traps](harness-traps.md). ## Migrating to v5 Source: https://goldziher.github.io/ai-rulez/migration-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](CHANGELOG.md). 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](standards.md) 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 1. Commit or stash your work, then preview: `ai-rulez migrate v5 --dry-run` prints the change list and writes nothing. 2. Run `ai-rulez migrate v5`. It rewrites the project in place (add `--recursive` for a monorepo with several `.ai-rulez/` directories). `--check` exits 2 while anything still needs migrating, for CI. 3. Run `ai-rulez doctor`. It reports removed presets with a replacement, drifted outputs and generated paths git does not ignore. 4. 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: ```bash 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.]` (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 `.gitignore` block, content-only headers). Regenerate and review the diff. - `--write`: also rewrite frontmatter aliases in the `.ai-rulez` markdown sources. - `--recursive`: migrate every project found below the current directory. - `--format json`: a machine-readable report (`schema_version`, one entry per project with `status`, `changes`, `warnings` and `error`). 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.** A `4.x` config is rejected with `run ai-rulez migrate v5`; `2.x`/`3.x` with 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`, the `config.local.*` forms and the YAML or JSON form of `mcp.*`). Any command that finds one stops with `YAML 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.json` files are no longer read.** *Migrate:* merges them into `config.toml`. `ai-rules-mcp.schema.json` is removed. - **`init --format yaml|json` is removed**; `init` writes `config.toml`. - **`migrate v4` is removed.** `migrate` takes one target, `v5`. - **`[lint.budget]` is now `[lint.ratchet]`** (and `ratchet_exceeded` in the JSON report, `Over ratchet` in Markdown). It never meant a size limit; `[lint.budgets.]` is. *Migrate:* renames the table. *Before:* ```toml version = "4.0" [lint.budget] AR401 = 3 ``` *After `ai-rulez migrate v5`:* ```toml 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 = true` by default.** `AGENTS.md` is the single canonical instruction file (root and nested scopes); `CLAUDE.md` is a shim that imports `@AGENTS.md`; skills go to `.agents/skills` where the harness reads them. Set `agents_md = false` for the old per-harness files. *Migrate:* pins `agents_md = false` unless you pass `--adopt-defaults`. - **Generated headers carry only the per-file `Content-Hash`.** The project-wide `Source-Hash` line, which rewrote every generated file on any edit, is gone by default. `[header] hashes = "full"` brings it back and `"none"` drops both. *Migrate:* pins `hashes = "full"`. - **The managed `.gitignore` block is opt-in.** `gitignore` defaults 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 --gitignore` or `gitignore = true` turns the block on. *Migrate:* pins `gitignore = true`. ##### Commands and flags - **`validate` runs the content checks by default.** What `validate --strict` did is now plain `validate` (globs that match nothing, dead links, missing hooks, oversize content, the security rules). `--config-only` keeps the old config-only behavior. `--strict` now means *warnings fail*, the same as `--fail-on warning`, as it already did for `doctor` and `verifiers run`; it cannot be combined with `--config-only` or another `--fail-on`. A CI job that ran `ai-rulez validate` and 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 with `validate --config-only`. Replace `validate --strict` by `validate` in scripts (keep `--strict` only when warnings should fail). - **`usage ...` and `report usage|evals` are folded into `telemetry ...`, 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 ` | `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 ` (`--config-dir`, on most commands) | `--config-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 ` (sign an organization policy) | `sign --org-policy `; `--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 ` 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](cli.md#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](cli.md#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](cli.md#json-contracts) 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](cli.md#exit-codes). `lock` keeps its documented codes. ##### Output streams, errors and environment - **Results go to stdout, diagnostics to stderr, and `-q` never hides a result.** `list rules|context|skills|agents|commands|checks`, `domain list`, `profile list`, `include list`, `skill list`, `clean --dry-run` and `lock --check` used to print through the logger on stderr, so `-q` erased them. They now print on stdout. *Migrate:* scripts that read stderr for these lists read stdout, or use `--format json`. `-q` now 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: `, an optional `Validation errors:` list and `Hint: ` on stderr. The `ERROR Failed to ... error=... hint=...` log-style errors of the `add`, `remove`, `list`, `domain`, `profile` and `include` family, the doubled `ERROR` plus `Error:` lines of `validate` and `scan`, and the bare messages of `review`, `eval run` and `telemetry enable` are gone. Under `--format json` a failure also writes `{"status": "error", "error": ..., "hint": ..., "exit_code": n}` to stdout (it carries `schema_version` like every document). *Migrate:* scripts that grep stderr for the old wording match `Error:`; scripts that parse stdout under `--format json` now always get a document. - **Exit codes are unchanged** (`0` ok, `1` could not run, usage errors included, `2` findings or drift, `3` `lock` only), but are now produced in one place. A refused confirmation (`clean`, `remove`, `--yes` missing in a non-interactive shell) exits `1`, as before; `generate --user --clean` declining a prompt used to exit `0` and now exits `1`. - **Confirmation prompts are written to stderr**, not stdout, so a piped stdout is never polluted. - **Unprefixed environment variables are ignored.** `DEBUG`, `QUIET` and `VERBOSE` used to change the CLI's output (they were read by an unprefixed `AutomaticEnv`). Use `AI_RULEZ_DEBUG=1` / `AI_RULEZ_QUIET=1`. `AI_RULEZ_*` variables are listed in [the CLI reference](cli.md#environment-variables). - **`~/.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` / `-V` is removed** (it did nothing beyond that line). Use `--debug` / `-D`. - **`--config-dir` is a global flag.** Commands that declare their own `--config-dir` / `-n` keep it. `init` and `migrate` no 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 ` and `-C ` (they used to ignore both and only auto-detect `.ai-rulez`, then `.config/ai-rulez`). *Migrate:* a script that passed `--config-dir` to one of them and relied on it being ignored now acts on that directory. - **`--format text|json` on more commands:** `generate` (`--check`, `--dry-run`, the summary), `clean`, `sign`, `export okf` and `version`. ##### Outputs - **`[[plugins]]` no longer writes `.claude/plugins.json` or `.codex/plugins.json`.** No tool read them. The table is still accepted and `generate` warns that it has no effect. Use `[claude.settings] manage = true` with `enable_plugins` for Claude Code and `[plugins."name@marketplace"] enabled = true` in `.codex/config.toml` for Codex. *Migrate:* warns; delete the files it left behind. - **Every `--format json` document carries `schema_version`.** List commands (`list`, `domain list`, `profile list`, `include list`, `skill list`, `builtins list`) and multi-profile `tokens` now 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-diff` and `convert-report` (`*.schema.json`). A change to a document's shape bumps its `schema_version`. - **`ai-rulez.lock`, `[lock] enforce`, `http://` sources, `scan_imports`** and 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](strict-validation.md#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](cli.md#existing-files-generate-will-not-overwrite) | | `init --from` runs through `convert --write` | Expect one context item per root file (`convert --split-headings` splits it); see [`init --from`](cli.md#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](#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](#approvals) | | Go APIs under `internal/` changed; `pkg/airulez` is the supported API | See [Go API](#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](okf.md) 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: ```bash 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`, `title` and `x-ai-rulez` are reserved frontmatter keys. A native file that used `type` or `title` as its own key no longer sees it as metadata. - `index.md` and `log.md` that only list entries (headings, bullet links) are listings, not content. A rule that happens to be called `index` with prose in it is still a rule. - `validate` runs `okf validate` on a tree that has a root `index.md` and reports the `AR9B*` findings, failing at `--fail-on` (default `error`). - Not yet: `add`, `init` and the MCP CRUD tools still write the native layout, and a custom `title` is not restored by `export okf` from 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.md` becomes one context item. Use `ai-rulez convert --split-headings` to split it. - MCP files, hooks and permissions are imported too (hooks and `allow` rules 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 --from` leaves 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//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.` with `enabled`, not `mcp.servers.` 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, `generate` fails and names them instead of keeping the last one. Make the outputs agree, for example with `agents_md = true` or `rules.mode = "inline"`. See [Supported harnesses](harnesses.md#cross-cutting-behavior). `qoder` next 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.json` no longer receives `mcpServers`, so resolved env values do not land in a committed file. An entry an earlier version wrote there is removed on the next `generate` or `clean` while 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. - **`generate` warns about unknown config keys** in `config.toml` and `config.local.toml`, naming the nearest known key. `generate --strict-config` (or `AI_RULEZ_STRICT=1`) fails instead. `validate` fails on them as before. - **`generate` summarises new or changed commands**: hook commands, command-based MCP servers, `[permissions] allow` rules, `[claude.settings.managed] env`, plugin enablement and `http`/`prompt` hooks 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 `--yes` or set `AI_RULEZ_ACK_COMMANDS=1` in CI to silence it. - **`clean` keeps hand-edited generated files** with a warning; `clean --force` removes them. The committed `.ai-rulez/.generated-manifest.json` lists merged-document paths only; claims and digests live in the gitignored `.generated-manifest.local.json`. A forged committed manifest cannot make `generate` or `clean` delete or strip anything: a file is removed only when its own `Content-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](checks.md) are never added to the managed `.gitignore` block. An entry an earlier version added is removed on the next `generate`. - **`generate --dry-run` prints `unchanged:` and `edited:`** for files that need no write. Scripts that match `write-file:` for every file need updating. - **`tokens` totals include the item listing.** `always` and `headline_always` now count the skill, command and agent listing, so `--budget` can fail where it passed. The previous figures are `always_legacy`, `conditional_legacy` and `headline_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.dir` and `marketplace.output_dir` are rejected when they contain `..`, are absolute or drive-qualified, or name `.git`, `.ai-rulez`, `.hg` or `.svn`. `generate` fails 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 as `exec-file` in the command summary. #### Lock file and enforcement - **`ai-rulez.lock` pins content, under one hashing scheme.** `lock` now 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, as `sha256:` 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 --check` exits 2 on a lock without content pins, whatever `[lock] enforce` says. A lock with another `version` is refused with the instruction to run `ai-rulez lock` again. A lock that pins role outputs (`[roles]` `pin = true`) is written as `version = 2`, which an older v5 build refuses instead of misreading the role pins; a lock without role pins stays `version = 1`. See [Lock file](lockfile.md). - **Run `ai-rulez lock` once after upgrading**, review the diff and commit the file. The lock reads as stale until you do, for these reasons: - The `tree` digest now also covers the `source`, `ref` and `path` of remote entries and the `view` of served entries, so relabeling or swapping entries is detected. A lock written by an earlier v5 build reports the tree digest as stale. - `lock` pins the project scripts run by agent, skill and command frontmatter `hooks`. Editing such a script makes `lock --check` exit 2; locks of items with frontmatter hook scripts need `lock` once. - Local-path includes are pinned as `local-include` items, and include skills are recorded as `include:/` 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 next `lock`. - **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] enforce` defaults to `true` whenever `ai-rulez.lock` exists.** Set `enforce = false` to opt out. A remote include or installed skill the lock does not cover makes `generate` fail (as `--locked` always did) and `AR010` an error, and an include that cannot be resolved is an error instead of a skipped warning. `generate --frozen` and `--locked` are unchanged: they require the lock whether or not enforcement is on. - **`[[skills]]` is not a config key.** `skills` is 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//SKILL.md`, install them with `ai-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`, `generate` and `generate --check` exited `0` while rendering without it. They now exit `1` and name the include. `--no-fetch` keeps the old behaviour (a warning, the include skipped). - **`[lock] enforce = true` is strict.** It makes `validate --strict` report `AR981` (source drift) and `AR982` (output drift), makes `generate --locked` fail 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 another `version` or a source that cannot be snapshotted is an `AR981` finding, not a logged skip. - **`generate --check` verifies authored content against an enforced lock**, like `--locked`. - **The lock digest ignores the project-wide `Source-Hash` header**, 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, `lock` exits `3`, and `lock --refuse-findings` restores the fail-without-writing behavior. - **`lock` and `update` scan what they pin.** Every remote tree pinned to something new is scanned (`AR001`-`AR009`) first; an error finding refuses the pin (exit `2`, nothing written) unless `--accept-findings`. - **`generate --check` classifies machine-local inputs like `generate`.** With a `config.local.*` overlay or `local/` content it no longer reports every output as `stale`. A shared file the local input would change, and that `generate` refuses to write (tracked or not ignored), is reported as `blocked: ` with exit `2`; `--allow-local-drift` accepts 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://` and `git://` remotes are rejected.** `git://` is unauthenticated and can be rewritten in transit. A remote include, OKF include, installed skill or skill source must use `https://`, `ssh://` / `git@host:path`, or a local `file://` URL or path. The error names the source and says to switch to `https://`; `include add` refuses them too. Include sources accept a leading `git+` (`git+https://host/org/repo`). - **The git token goes only to allowlisted hosts.** `AI_RULEZ_GIT_TOKEN` / `--token` is sent to `github.com` by default, or to the hosts in `AI_RULEZ_GIT_TOKEN_HOSTS` (comma separated, environment only; when set it replaces the default, so list `github.com` too), as a host-scoped header rather than inside the URL, and only over `https://`. 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_TOKEN` or `gh auth token`) only to `github.com` or the hosts in `AI_RULEZ_FORGE_HOSTS` (comma separated, environment only), never to every `AI_RULEZ_GIT_TOKEN_HOSTS` host. For GitHub Enterprise Server, add its host to `AI_RULEZ_FORGE_HOSTS`. See [Forge client](forge.md). - **A committed config cannot point outside the project.** A local include (`source` or `local_override`) that resolves outside the project after symlinks (`../victim`, an absolute path) is a fatal error. It is still allowed in `config.local.toml` and the user config. A local `[[skill_sources]]` `url` or `path` must resolve inside the project (`mcp --source ` and the user config may point anywhere). `local_override` on an include or installed skill in the committed config is refused under `generate --locked`, `--frozen` and an enforced lock, because it bypasses the pins; set it in `config.local.toml`. - **Imported content is scanned by default.** `[lint.security] scan_imports` is on when unset: `generate` scans 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. Set `scan_imports = "off"` to opt out, or `"warn"` to log only. - **Unpinned MCP packages (`AR012`)** stay a warning in `validate`, and are an error whenever `[lock] enforce` is on (whenever `ai-rulez.lock` exists, unless `enforce = false`), together with `AR010`. - **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 `$HOME` dotfiles repository is held to its own directory. `GIT_CEILING_DIRECTORIES` entries are resolved through symlinks, as git does. A project not yet added to its repository has its own directory as the root until `git add`. A symlinked `config.toml` or `config.local.toml` follows 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`, and `ai-rulez validate` reports 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 symlinked `SKILL.md` is refused). `init --from` never follows symlinks. Repository content read at load time is capped at 8 MiB per file; a larger file is an error. - **`scan` is security-only.** `ai-rulez scan` runs only the `security` analyzer's checks. Hook and config findings (`AR504`, `AR9K0`, ...) that used to appear in its report belong to `validate --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](strict-validation.md#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](policy.md). #### 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-review` records 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_association` is `OWNER`, `MEMBER` or `COLLABORATOR`, or `approvers` or 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_age` is a ceiling.** An approval stops counting (`AR712`) once `approved_at` plus `max_age` has passed, whatever its `expires` says, and `approve --expires` beyond it is refused. See [Approvals](approvals.md). #### 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](embedding.md)): - The process-wide preset registry is gone: `config.GetPresetGenerator`, `config.PresetRegistry`, `config.RegisterPreset` and `config.RegisterRulesDir` are removed; each generation carries its own registry. - `config.LoadConfig` fails when the config declares includes or installed skills unless the load is given `config.WithResolvers` (or `config.WithoutRemote`). - `config.SetPolicyEnforcer` is 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 with `Options.WithLocal`; `Options.WithoutLocal` is 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](llm.md) and [Telemetry](telemetry.md). 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](trust-model.md). #### Hooks, validation and the rule registry - **`validate --strict` has 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](strict-validation.md); lower or turn off a rule with `[lint.severity]`, or accept current findings with `validate --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.]` (size budgets). `[lint.budget]` still works and warns. The report says `over its tolerated count of N`; the JSON key `budgets_exceeded` is unchanged. - **Top-level `[[hooks]]` validation is stricter**: a `script` outside `A-Za-z0-9._/-` is rejected, a missing or non-executable script is `AR504`/`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 through `ai-rulez mcp --serve-skills`; a static reference to one is `AR990`. Skills default to `static`, so nothing changes until you opt in. - **Eval results are signed per user.** `eval run` signs 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, is `unverified`: `eval run` re-runs it, and `AR997`, `AR998`, `report evals` and `report usage` ignore it. CI has no key; gate on `eval run --force`. See [Evals](evals.md). - **Usage log version 3.** The `session` field is a salted hash of the harness session id, not the raw id, and lines carry `digest`, `digest_scheme` and `event_id` (the lock's skill digest). Version 1 and 2 logs still read. - **`--json` is replaced by `--format json`.** Every command that printed JSON takes `--format text|json` (some add `sarif`, `junit`, `markdown`); `--json` stays as a hidden alias that warns. An unknown `--format` value is rejected with the allowed values. `lock --format` is accepted only with `--check`, `--diff`, `--outdated` or `--subject`. - **`schema/catalog.schema.json` is catalog version 2.** The version 1 schema moved to `schema/catalog.v1.schema.json`; `catalog --format json` still prints version 1 unless `--schema-version 2`. #### Where to look - [Supported harnesses](harnesses.md): the preset list and what each writes. - [Hooks, permissions and settings keys](settings.md), [Permissions](permissions.md) and [User-level configuration](user-scope.md): top-level `[[hooks]]`, `[permissions]` and `generate --user`. - [Lock file](lockfile.md) and [Trust model](trust-model.md): the lock scheme and what a repository may set. - [Changelog](CHANGELOG.md): the complete list for 5.0.0. ## Configuration Source: https://goldziher.github.io/ai-rulez/configuration/ Configuration reference for `.ai-rulez/config.toml` (YAML and JSON configs are no longer read; `ai-rulez migrate v5` converts them). #### File-Based Configuration ai-rulez uses a file-based approach where you edit files directly with your editor or use CRUD commands: - **Configuration**: Edit `.ai-rulez/config.toml` (TOML format) with any text editor - **Rules**: Add/edit `.ai-rulez/rules/*.md` files or use `ai-rulez add rule` - **Context**: Add/edit `.ai-rulez/context/*.md` files or use `ai-rulez add context` - **Skills**: Add/edit `.ai-rulez/skills/{name}/SKILL.md` files or use `ai-rulez add skill` - **Commands**: Add/edit `.ai-rulez/commands/{name}.md` (flat form) or `.ai-rulez/commands/{name}/COMMAND.md` (directory form with optional `references/` subdirectory) - **Checks**: Add/edit `.ai-rulez/checks/{name}.md` code-review guidelines (frontmatter `description`, `severity`, `tools`, `targets`) or use `ai-rulez add check`; see [Checks](checks.md) - **Agents**: Add/edit `.ai-rulez/agents/*.md` files or use `ai-rulez add agent` (`add command` likewise creates commands). The `claude` preset passes the documented Claude Code subagent keys (`disallowedTools`, `permissionMode`, `memory`, `maxTurns`, `mcpServers`, `hooks`, `background`, `isolation`, `color`, `initialPrompt`, `omitClaudeMd`) through with their YAML types; other presets ignore them. `validate` and `generate` warn about an agent key no tool reads (with a suggestion) and about a `skills:` entry that names no skill; `validate` reports the same findings as `AR303` and `AR302` - **Domains**: Add/edit `.ai-rulez/domains/{name}/{rules,context,skills,agents,commands,checks}/*.md` files or use `ai-rulez domain add` - **MCP Servers**: Inline in `.ai-rulez/config.toml` - **Machine-local configuration**: Personal content under `.ai-rulez/local/` and a `config.local.toml` overlay, both gitignored. See [Local overlay](#local-overlay) You can either directly edit files with your editor or use CRUD commands for programmatic modification. After changes, run `ai-rulez generate` to create tool-specific outputs. #### Configuration Discovery When no explicit path is given, the CLI discovers configuration by walking up from the current directory and trying, in order: 1. `.ai-rulez/config.toml` — the tool-specific directory (default) 2. `.config/ai-rulez/config.toml` — the project-level [`.config/` convention](https://github.com/pi0/config-dir) `config.toml` is the only config format read, and its `version` must be `"5.0"`. A project that has only a `config.yaml`, `config.yml` or `config.json` (or a `config.local.yaml`, `.yml` or `.json` overlay), a flat `ai-rulez.yaml` (also `.ai-rulez.yaml`, `ai_rulez.yaml` and their `.yml` forms), or a `config.toml` with an older `version`, fails with an error that names the file; run `ai-rulez migrate v5` to convert it (a 2.x or 3.x project goes through ai-rulez 4.x first). See [Migrating to v5](migration-v5.md). `.ai-rulez/` wins when both directory layouts exist at the same level. `--config ` selects an exact file, and `--config-dir ` selects a non-default directory (for example `--config-dir .config/ai-rulez`). To scaffold the `.config/` layout, run `ai-rulez init --config-dir .config/ai-rulez`; generated outputs and the managed `.gitignore` block then reference `.config/ai-rulez/` instead of `.ai-rulez/`. #### Checks `generate` runs before it writes `generate` runs the same schema check as `ai-rulez validate` over `config.toml` and the machine-local `config.local.toml`. A key the schema does not know (`delivry`, `[lock] enforc`) has no effect, so `generate` prints a warning naming the file and the key, with the nearest known key when there is one: ```text WARN Configuration problem: .ai-rulez/config.toml: unknown key "lock.enforc" (did you mean "enforce"?) ``` `generate --strict-config`, or `AI_RULEZ_STRICT=1` in CI, fails with exit code 1 instead. `validate` and `generate` report the same unknown keys. `--strict` means something different on each command, and they do not imply one another: | Command | `--strict` adds | | --- | --- | | `generate --strict-config` | Schema check only: an unknown or invalid configuration key fails the run instead of warning. | | `validate` | Deep content checks (dead links and references, globs that match nothing, size and duplicate checks, findings `AR###`). Unknown keys already fail `validate` without it. | Run both in CI: `ai-rulez validate && ai-rulez generate --strict-config`. `generate` also lists the commands it is about to write that your tools will run: `[[hooks]]` commands (and the content digest of a hook `script` file, so a changed script behind the same path is listed again), `http` and `prompt` hooks, command-based `[[mcp_servers]]`, `[permissions] allow` rules for every harness that takes them, `[claude.settings.managed] env` entries (`NODE_OPTIONS`, `ANTHROPIC_BASE_URL`, ...), and plugin enablement and marketplace registration. Only commands that are new or changed since the previous run on this machine are listed (every command on a first run in a fresh clone), one item per line. It only warns, it is printed even with `--quiet`, and `--yes` or `AI_RULEZ_ACK_COMMANDS=1` silences it. The previous run is remembered in `~/.cache/ai-rulez/commands/`, never in the repository. The text is repository-controlled, so it is made safe to print: control characters (ESC, CR, LF, other C0/C1 codes), bidirectional overrides and zero-width characters appear as visible escapes (`\x1b`, `\u202e`), and anything that looks like a credential is replaced by `` (secret-named flags such as `--token=...`, `Bearer`/`Authorization` values, URL user information and query values, `NAME=value` with a secret-looking name, well-known token prefixes, long base64 or hex runs). An env value is shown unless its name or value looks like a credential. `generate --watch` prints the same list on every regeneration that changes a command, and the MCP `generate_outputs` tool returns it as `new_commands` (and writes it to stderr) because it has no terminal. #### Basic Structure The minimal valid configuration: ```toml version = "5.0" name = "my-project" ``` A typical production configuration: ```toml version = "5.0" name = "my-project" description = "My project description" presets = ["claude", "cursor", "gemini"] default = "full" gitignore = true [profiles] full = ["backend", "frontend", "qa"] backend = ["backend", "qa"] frontend = ["frontend", "qa"] [[mcp_servers]] name = "ai-rulez" command = "npx" args = ["-y", "ai-rulez@latest", "mcp"] ``` #### Required Fields ##### `version` The config schema version. Must be `"5.0"`; `"4.0"` and older are rejected with a pointer to `ai-rulez migrate v5`. ```toml version = "5.0" ``` ##### `name` The project name. Used in generated files and displayed in headers. ```toml name = "acme-platform" ``` Every project has one name; the value is used verbatim in generated headers. #### Content Layout ##### Symlinks in content A symlinked file or directory under your project's `.ai-rulez/` (including `domains/` and `local/`) is followed only when its fully resolved target is inside the project (the git top-level, else the directory holding `.ai-rulez/`). Any other symlink, including a dangling one, is refused: `generate` fails with an error naming it and `ai-rulez validate` reports it as an error. Content from includes, installed skills, skill sources and OKF bundles never follows symlinks; see [Includes](includes.md). ##### Skills Skills use a directory form with supporting resources: ```text .ai-rulez/skills/deployment-checklist/ ├── SKILL.md # Main skill content ├── references/ # Markdown documentation (optional) │ └── api-endpoints.md ├── scripts/ # Executable scripts (optional) │ └── deploy.sh └── assets/ # Binary assets (optional) └── diagram.png ``` The skill directory name becomes the skill id. Resources under `references/`, `scripts/`, and `assets/` are emitted as separate files in the generated output, preserving the Agent Skills progressive-disclosure model. Subdirectories outside these three produce a warning naming the skill and the unrecognized directory. Build artifacts (`.venv*`, `__pycache__`, `node_modules`, anything `.gitignore` ignores, ...) are not bundled; see [`bundle_exclude`](#bundle_exclude). ##### Commands Commands support both flat and directory forms: **Flat form** (single file): ```text .ai-rulez/commands/ ├── deploy.md └── review.md ``` **Directory form** (with supporting resources): ```text .ai-rulez/commands/deploy/ ├── COMMAND.md # Main command content └── references/ # Markdown documentation (optional) └── deployment-guide.md ``` The directory form mirrors the skill layout and supports `references/`, `scripts/`, and `assets/` subdirectories. Use it when a command needs bundled reference material. #### Optional Fields ##### `description` Brief description of the project or configuration. ```toml description = "SaaS platform with React frontend and Go backend" ``` ##### `presets` Specifies which tools to generate configuration for. Can be built-in preset names or custom preset objects. ###### Built-in Presets ```toml presets = [ "claude", # → CLAUDE.md, .claude/ (rules/, skills/, agents/) "cursor", # → .cursor/rules/, .cursor/commands/, .cursor/agents/, .agents/skills/ "gemini", # → GEMINI.md, .gemini/ (settings.json, agents/), .agents/skills/ "copilot", # → .github/copilot-instructions.md, .github/instructions/, .github/{skills,agents,commands}/ "devin", # → AGENTS.md, .devin/ (rules/, skills/, agents/, mcp_config.json) "cline", # → .clinerules/, .cline/ "codex", # → AGENTS.md, .agents/skills/ and .codex/ "amp", # → AGENTS.md and .agents/ (.amp/settings.json when an effort resolves) "junie", # → AGENTS.md and .junie/ (rules/, skills/, agents/) "opencode", # → AGENTS.md, .opencode/, opencode.json (when MCP servers are set) "hermes", # → .hermes.md "antigravity", # → .agents/ (rules/, skills/, agents/), GEMINI.md "xum", # → AGENTS.md, .xum/skills, .xum/agents, .xum/mcp.jsonc (stdio with env as a shell prefix, http and sse MCP servers) "pi", # → AGENTS.md, .agents/skills, .pi/agents, .pi/mcp.json (stdio and http MCP servers) "baz", # → AGENTS.md (root and nested), .agents/skills, .claude/agents; see baz.md "okf" # → docs/okf/, an Open Knowledge Format bundle of the content; see okf.md ] ``` These are 15 of the 52 built-in presets. The complete list, with the features each supports, is in [Supported harnesses](harnesses.md); the names are: `aiassistant`, `amp`, `antigravity`, `augment`, `baz`, `bob`, `claude`, `cline`, `codebuddy`, `codebuff`, `codewhale`, `codex`, `commandcode`, `copilot`, `copilot-cli`, `cortex`, `crush`, `cursor`, `deepagents`, `devin`, `dsh`, `factory`, `gemini`, `gitlab-duo`, `goose`, `grok`, `hermes`, `junie`, `kilo`, `kimi`, `kiro`, `letta`, `mimocode`, `muse`, `omp`, `openclaw`, `opencode`, `pi`, `poolside`, `qoder`, `qwen`, `reasonix`, `replit`, `rovodev`, `takt`, `trae`, `vibe`, `warp`, `xum`, `zcode`, `zed`, `zoocode`. The `windsurf` preset is now `devin` and `continue-dev` was removed; `ai-rulez doctor` reports both. See [Migrating to v5](migration-v5.md). With `agents_md = true`, one shared `AGENTS.md` and `.agents/skills/` replace the per-tool copies; see [`agents_md`](#agents_md). With the default `[rules] mode = "split"`, rules are written to each tool's native rules folder (`.claude/rules/`, `.cursor/rules/`, `.github/instructions/`, `.junie/rules/`, `.agents/rules/`, and so on) and the root file keeps context and the agent roster. See [Rules](rules.md#rules-mode) for what each preset writes in each mode. `mcp` is a built-in preset too, but it is a shared utility: it writes the generic `.mcp.json` and is invoked automatically when MCP servers are configured, so you normally do not name it. It is the only preset not produced by a coding-tool adapter, and `presets = ["mcp"]` alone generates nothing else. ###### Custom Presets For tools not in the built-in list: ```toml [[presets]] name = "my-tool" type = "markdown" # or: directory, json path = "docs/MY_TOOL.md" template = """ # {{ .Name }} {{ range .Rules }} - **{{ .Name }}**: {{ .Content }} {{ end }} """ ``` The template uses Go's `text/template` with only its built-in functions; a helper such as `where`, `truncate`, or `now` is not defined and fails to parse. See [Custom Presets](profiles.md) for the available template data. ###### Provider-backed Presets (full parity) A custom preset may reference a declarative **provider spec** instead of a template. A provider spec has the same expressive power as the built-in presets — root instructions file, skills/agents/commands, frontmatter, effort/model, and MCP sidecars — and is validated against [`schema/provider.schema.json`](https://github.com/Goldziher/ai-rulez/blob/main/schema/provider.schema.json). ```toml [[presets]] name = "my-tool" provider = ".ai-rulez/providers/my-tool.toml" ``` The `provider` path is relative to the project root and must not escape it. The spec's `name` must match the preset's `name`. Example spec: ```toml name = "my-tool" directories = [".my-tool"] [root] file = "MY_TOOL.md" sections = ["title", "rules_inline", "context_inline"] [outputs.skills] mode = "per_item_file" dir = ".my-tool/skills" filename = "{id}/SKILL.md" [[sidecars]] kind = "mcp_json" path = ".my-tool/mcp.json" emit_when = "has_mcp_servers" # also: always, has_plugins, has_resolved_effort, has_mcp_json_entries, has_mcp_servers_or_plugin_settings, has_resolved_effort_or_mcp_servers, has_hooks ``` Built-in presets are written as plain strings (`presets = ["claude", "xum"]`); provider-backed presets use the inline-table form shown above. ###### Output path rules A configuration comes from the repository, so every output path it names (a custom preset `path`, a provider spec's `root.file`, `outputs.*.dir`, `sidecars[].path` and `directories`, `okf.dir`, `marketplace.output_dir`, `codex_skills_dir`) must be a relative path inside the project. `validate` and `generate` reject an absolute path, a `..` segment, a drive or UNC prefix, any `.git` segment (any case), and a path under `.ai-rulez/`, `.config/ai-rulez`, `.hg` or `.svn`. A custom preset that writes a file CI or a developer tool runs (`.github/workflows/`, `Makefile`, `.vscode/tasks.json`, ...) is allowed, and the `generate` command summary lists it as `exec-file` before writing. Generation also refuses any write that resolves outside the project or inside `.git`. ###### Native tool and model names in a provider spec A tool with its own tool and model vocabulary can be given a translation instead of Claude's names. These keys go under `[outputs..frontmatter]`: | Key | Description | | ------------------- | ----------- | | `tool_names` | Table of Claude tool name to the tool's own name for the `tools` list (matched ignoring case). A tool the table does not name is dropped; an agent left with none writes no `tools` key | | `tool_case` | `"lower"` writes every tool name lower-case (after `tool_names`) | | `model_aliases` | Table of resolved model (matched ignoring case) to the tool's own id; `""` drops the model | | `drop_bare_aliases` | Drops a model that is `sonnet`, `opus`, `haiku` or `inherit`, so the agent inherits the session model; runs after `model_aliases` | `[outputs..body]` takes `replace` (a table of placeholder to replacement in the item's content, such as `{ "$ARGUMENTS" = "$prompt" }`) and `replace_flag` (a frontmatter key written `true` when a replacement happened). An `mcp` sidecar takes `transports` (`["stdio"]` leaves remote servers out), `env_ref_syntax` (`"dollar"`, `"env_prefix"`, `"braced"` or `"opencode_env"`: a value that came from a `${VAR}` placeholder is written as `$VAR`, `${env:VAR}`, `${VAR}` or `{env:VAR}` instead of the resolved secret), and `elements.project_only` (the elements are project-relative and skipped in the user scope). A `hooks` sidecar renders the top-level `[[hooks]]` into the tool's own hooks file. Its `dialect` names the harness whose hook format is used (`claude`, `qwen`, `factory`, `kiro`, `vibe`, ...; the harnesses and what each writes are in [Hooks and permissions](settings.md)), which fixes the key and the layout, so a hooks sidecar takes no `key`; it is emitted when `[[hooks]]` are declared (`emit_when = "has_hooks"`): ```toml [[sidecars]] kind = "hooks" dialect = "qwen" path = ".qwen/settings.json" global_path = ".qwen/settings.json" ``` Sidecars that name one `path` (an `mcp` and a `hooks` sidecar of one settings file) are merged into a single document, so neither replaces the other. ###### Split rule files in a provider spec A provider can honour the `[rules] mode` setting like the built-in `claude` and `junie` presets by adding a `split` rules output: ```toml [root] file = "MY_TOOL.md" sections = ["title", "rules_inline", "context_inline"] [outputs.rules] mode = "per_item_file" dir = ".my-tool/rules" filename = "{id}.md" split = true dialect = "claude" # claude | cursor | trigger | copilot | cline | continue | junie inline_filter = "path_scoped" ``` | Field | Description | | --------------- | ----------- | | `split` | When `true`, the output follows `[rules] mode`: split mode writes every rule (and path-scoped context item) to a file, inline mode writes only the items `inline_filter` selects | | `dialect` | Required with `split`. The frontmatter vocabulary of the rule files; cannot be combined with `body` or `frontmatter` | | `inline_filter` | Optional, `path_scoped` only. Items that still get a file in inline mode; omit to keep everything inline | | `dir` | Required with `split`: a relative path inside the project | | `filename` | Flat `{id}` template, no `/` | `split` also requires `rules_inline` in `root.sections` and cannot be combined with `filter`. The folder is treated like the built-in rules folders: hand-written files are never overwritten, hashes go in the banner, and, with `gitignore = true`, generated files are gitignored one by one. A provider without `split` inlines all rules in its root file. Custom providers get no [local](local-overrides.md) output. ##### `default` The default profile name used when `ai-rulez generate` is run without `--profile`. ```toml default = "full" ``` It may name several profiles to compose, the same as `--profile`: ```toml default = "base,backend" ``` When `default` is not set, the built-in `default` profile applies and its meaning depends on whether any profiles are defined: - If **no profiles** are defined at all, `default` includes root content and **all** domains. - If profiles are defined and one is named `default`, that definition is used. - If profiles are defined but none is named `default`, `default` includes root content, globally-active builtin domains, and domains sourced from external includes — but not profile-only domains. ##### `profiles` Named profiles that specify which domains to include in generation. ```toml [profiles] full = ["backend", "frontend", "qa"] backend = ["backend", "qa"] frontend = ["frontend", "qa"] qa = ["qa"] ``` Each profile specifies a list of domain names. When generating with a profile: 1. All root content (`.ai-rulez/rules/`, `.ai-rulez/context/`, `.ai-rulez/skills/`, `.ai-rulez/agents/`, `.ai-rulez/commands/`) is included 2. Content from the specified domains (`.ai-rulez/domains/{name}/`) is included 3. Globally-active builtin domains and every domain sourced from an external include are always included, whatever the profile 4. A builtin named as `builtin:` in the profile is loaded for that profile only Several profiles can be selected at once by separating them with commas — `--profile base,backend`, or `default = "base,backend"` — which generates the union of their domains. A profile name may therefore not contain a comma, and a profile's value lists domains only, never other profiles. See [Domains and Profiles](domains.md#composing-profiles) and, for how profiles differ from roles, [Profiles vs roles](roles.md#profiles-vs-roles). ##### `roles` and `role_manifest` `[[roles]]` maps a job to a slice of the content (domains, per-kind include/exclude selectors, inheritance, a per-skill `skill_mode` rendered as Claude Code `skillOverrides`), selected with `ai-rulez generate --role `. Like `--profile`, `--role` accepts a comma-separated list to compose several roles; see [Composing roles](roles.md#composing-roles). `[role_manifest] enabled = true` writes `/roles.json`; `skill_mode_fallback = "drop" | "serve"` says what an `off` skill does on a harness with no setting for it. See [Roles](roles.md). ```toml [role_manifest] enabled = true [[roles]] name = "backend" domains = ["backend"] [roles.skills] exclude = ["deploy-*"] [roles.skill_mode] migrate = "name-only" ``` ##### `lock` `[lock]` tunes how strictly `ai-rulez.lock` is enforced: `enforce` (default `true` whenever `ai-rulez.lock` exists, `enforce = false` opts out) makes `validate` report content drift (or an unreadable lock) as `AR981` / `AR982`, makes `generate` refuse a remote include or installed skill the lock does not pin (and `AR010` an error), and makes `generate --locked` require content pins (`lock --check` always does); `include_outputs` (default `true`) pins generated outputs; `scope` is `all` (default) or `skills`. See [Lock file](lockfile.md). An [organization policy](policy.md) can force `enforce` and `include_outputs` on. ##### `scopes` Generate additional assistant files in subfolders with their own profile. ```toml [profiles] frontend = ["frontend"] [[scopes]] path = "packages/web" profile = "frontend" presets = ["codex", "claude"] ``` A scoped file contains **only** the scope's own content (its profile's domains that the root run does not already render), not the root rules and context: the target tools load a subdirectory `CLAUDE.md`/`AGENTS.md` on top of the root file, so repeating the root content would duplicate the always-loaded text. The default scoped presets are `codex` and `claude`, producing subfolder `AGENTS.md` and `CLAUDE.md` files. A domain that the root output already contains (a builtin, an include, or one selected by the root profile) is never repeated in a scope, and a scope left with nothing writes no files. Scope paths must be relative, stay inside the project and contain no glob characters (`* ? [ ] { } , !`); `validate` rejects anything else. Rule files of a scope are written to the root rules folders with the scope path as a qualifier and glob prefix, not into the scope directory; see [Scoped Rule Files](monorepo.md#scoped-rule-files). ##### `gitignore` Opt-in: controls whether `ai-rulez` maintains a managed block in `.gitignore` with generated output patterns. It is off by default, because committed outputs should not flip-flop in and out of the ignore block. ```toml gitignore = true # Maintain the managed .gitignore block (or pass `generate --gitignore`) # gitignore = false # Default: leave .gitignore alone ``` When `true`, ai-rulez adds the specific generated files and owned subdirectories to `.gitignore` — `AGENTS.md`/`CLAUDE.md`/`.mcp.json` and owned subtrees such as `.claude/skills/`, `.claude/agents/`, `.codex/`, `.opencode/skills/`. It never ignores an assistant directory wholesale, so a hand-authored `.opencode/settings.json` beside the generated files stays tracked. GitHub output is narrower: ai-rulez ignores generated `.github/copilot-instructions.md`, `.github/agents/`, `.github/prompts/`, and `.github/skills/` without ignoring all of `.github/`. ai-rulez adds only what git does not already ignore. Before writing, it asks git (`git check-ignore`) about each entry against every ignore source (all `.gitignore` files, `.git/info/exclude`, `core.excludesFile`) with its own block left out, and skips an entry when a rule of yours already ignores it, or when your last matching rule is a negation (`!CLAUDE.md`), which is read as a deliberate override and never re-ignored. The managed `# BEGIN ai-rulez` / `# END ai-rulez` block holds only the remainder and is removed when nothing is left. A negated machine-local or secret output (`*.local.*`, `config.local.*`, `.ai-rulez/local/`, a generated MCP config holding secrets) is also left out of the block, with a warning; generation still refuses to write a secret-bearing MCP config that ends up unignored. Outside a git repository, or when git is unavailable, every entry is added. Machine-local outputs and sources are **always** gitignored, even when `gitignore = false`; an entry is added when the file or directory exists, so a project without a `config.local.*` overlay or `.ai-rulez/local/` tree gets no line for it: the local outputs (`CLAUDE.local.md`, `AGENTS.local.md`, `GEMINI.local.md`, `AGENTS.override.md`, `/*.local.*` including `ai-rulez.local.*`), the `.ai-rulez/local/` source tree, the `config.local.*` overlay with its `.config.local.*` lock and temp files, and `.ai-rulez/.generated-manifest.local.json`. Overlay-derived outputs whose names differ per machine are excluded through `.git/info/exclude` instead. See [Local Configuration](local-overrides.md). `gitignore` defaults to `false`, so generated files are normally committed; set it to `true` when the team keeps generated output out of git and regenerates it locally. ##### `compact` Controls whether generated inline rule sections omit per-rule `**Priority:**` annotations. ```toml compact = false # Default: include Priority lines # compact = true # Omit Priority annotations, reduce output size ``` When `true`, generated presets (CLAUDE.md, GEMINI.md, copilot-instructions.md, etc.) omit the per-rule `**Priority:**` lines, reducing file size for large rule sets. Applies to all configured presets. ##### `agents_md` Renders the files several tools share once instead of once per tool. On by default: `AGENTS.md` is the canonical instruction file and `CLAUDE.md` imports `@AGENTS.md`. ```toml agents_md = true # Default: one shared AGENTS.md and .agents/skills # agents_md = false # Every preset writes its own files ``` When `true` (the default), always-on rules and context go into one `AGENTS.md` (plus a nested `/AGENTS.md` for each `[[scopes]]` entry) and skills go into one `.agents/skills//SKILL.md`. The presets that read these files (`codex`, `opencode`, `amp`, `xum`, `pi`, `claude`, `gemini`, `antigravity`, `hermes`, `cursor`, `copilot`, `devin`, `cline` and `junie`) stop writing their own `AGENTS.md` copy, root file and skills directory. Turning the flag off regenerates the per-tool files and removes the shared ones that no preset writes itself. Quick reference: - Rules folders (`.cursor/rules`, `.claude/rules`, ...) keep only the items that are not always-on; the scoped items are also inlined into `AGENTS.md` when a configured preset has no folder for them (duplicated for tools that have one). - `claude` keeps `CLAUDE.md` as a banner plus `@AGENTS.md`; `gemini` gets `AGENTS.md` added to `context.fileName` in `.gemini/settings.json`; `GEMINI.md`, `.hermes.md` and `.github/copilot-instructions.md` are not written. - Skills with `targets`, machine-local files and agents, commands and MCP files stay per-preset. - If another preset (including a custom provider) also writes `AGENTS.md` or a file under `.agents/skills`, the shared output wins. Tool support, per-preset file lists, the duplication trade-off, `targets`, scopes and the on/off behavior are in [AGENTS.md and .agents/skills](agents-md.md). ##### `codex_skills_dir` Where the `codex` preset writes skills, relative to the project root. The default is `.agents/skills`, the directory Codex documents for repository skills (it scans `.agents/skills` from the working directory up to the repository root; `.codex/skills` is not read). Set it to `.codex/skills` to write there instead: ```toml codex_skills_dir = ".codex/skills" # Default: ".agents/skills" ``` After the default changes, `generate` writes the new tree and removes the files it previously wrote to `.codex/skills` (only manifest-tracked files; hand-written skills stay). The path must stay inside the project. With `agents_md` on (the default), skills are written once to `.agents/skills` whatever this is set to. ##### `bundle_exclude` Extra patterns for files that are left out of a skill's or command's bundled resources (`references/`, `scripts/`, `assets/`): they are neither listed in the generated `SKILL.md` `## Resources` section nor copied. Independently of this key the bundle skips `.git`, `.venv*`, `venv`, `__pycache__`, `*.pyc` and `node_modules`, and, when the project is in a git work tree, everything `.gitignore` ignores (tracked files and untracked files that are not ignored are kept). A skill directory that is itself gitignored is bundled without the `.gitignore` filter. ```toml bundle_exclude = ["*.log", "scripts/build/", "assets/raw/*.psd"] ``` A pattern without `/` matches any path segment (`*.log`, `tmp`); one with `/` matches the path relative to the skill directory, or a directory on it. Patterns use glob syntax (`*`, `?`, `[a-z]`). Skills fetched through `includes` and `installed_skills` get the built-in list and `.gitignore` only. ##### `mcp` Project-level MCP generation options. ```toml [mcp] self_server = true # default; add ai-rulez's own MCP server to .mcp.json # self_server_version = "5.0.0" # default: the running binary's version ("latest" for a dev build) # self_server_command = ["ai-rulez", "mcp"] # replace the whole launch command instead of npx ``` | Field | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `self_server` | When `true` (the **default in v5**), `generate` adds `"ai-rulez": {"type": "stdio", "command": "npx", "args": ["-y", "ai-rulez@", "mcp"]}` to the project `.mcp.json`, so a project is MCP-ready out of the box. Set `false` (or pass `generate --no-self-mcp`) to opt out. | | `self_server_version` | Version (or dist-tag) to pin. Defaults to the version of the ai-rulez binary running `generate`, or `latest` for a dev build. Only applies while the self server is on. | | `self_server_command` | Executable followed by its arguments, replacing the npx launch. Mutually exclusive with `self_server_version`. Only applies while the self server is on. | Behavior: - **On by default**: a project gets the `ai-rulez` MCP server without declaring anything; `self_server = false` or `generate --no-self-mcp` turns it off. - **Merged, not replaced**: with `self_server` and no `[[mcp_servers]]`, ai-rulez owns only the `mcpServers.ai-rulez` entry. Other servers in an existing `.mcp.json` (and every other key) are preserved, so running `generate` twice produces no diff. - **Every MCP writer agrees**: the root `.mcp.json` is rendered identically by the `mcp` preset and the cursor/copilot writers, and the `antigravity` preset adds the same entry to `.agents/mcp_config.json`. - **With `[[mcp_servers]]`**: declared servers keep their existing behavior (ai-rulez owns the whole `mcpServers` object, see [Settings document merge behavior](#settings-document-merge-behavior)), and the ai-rulez entry is added to it unless you declare a server named `ai-rulez` yourself, which wins. - **Per root**: each config root decides independently, including under `generate --recursive`. - The pinned version changes when you upgrade ai-rulez, so the generated `.mcp.json` changes with it. - **Turning it off**: `self_server = false` (or `generate --no-self-mcp`) stops ai-rulez from owning the entry but does not delete it; remove `mcpServers.ai-rulez` from `.mcp.json` yourself. `ai-rulez clean` likewise leaves a `.mcp.json` that holds hand-authored servers in place. ##### `mcp_servers` Inline MCP (Model Context Protocol) server definitions. A separate `mcp.yaml`, `mcp.toml` or `mcp.json` is no longer read. ```toml [[mcp_servers]] name = "ai-rulez" command = "npx" args = ["-y", "ai-rulez@latest", "mcp"] [[mcp_servers]] name = "grafana" command = "uvx" args = ["mcp-grafana"] env = { GRAFANA_URL = "http://localhost:3000", GRAFANA_SERVICE_ACCOUNT_TOKEN = "${GRAFANA_SERVICE_ACCOUNT_TOKEN}" } ``` Each entry supports: | Field | Required | Description | | ------------- | -------- | ----------------------------------------------------------------------------------- | | `name` | Yes | Unique server identifier | | `description` | No | Human-readable description of the server | | `command` | No | Command to run for local `stdio` servers (npx, uvx, ai-rulez, etc.). May contain `${PROJECT_ROOT}`. | | `args` | No | Array of command arguments for local servers. Elements may contain `${PROJECT_ROOT}`. | | `env` | No | Environment variables as key-value pairs. Values may contain `${VAR}` placeholders. | | `transport` | No | `stdio`, `http`, or `sse`. Defaults to `stdio`. | | `url` | No | Remote MCP server URL for `http` or `sse` transports. | | `headers` | No | HTTP headers for `http` or `sse` servers (e.g. auth). Values may contain `${VAR}` placeholders. | | `enabled` | No | Set to `false` to skip the server in generated MCP outputs. Defaults to `true`. | | `profiles` | No | Restrict the server to the named profiles. Omit to include it in every profile. | | `domain` | No | Restrict the server to a domain: it is included only when the active profile or role selects that domain. Omit to include it in every selection. | Local `stdio` servers normally need `command`; remote `http` and `sse` servers normally use `url`, plus `headers` when the server needs auth: ```toml [[mcp_servers]] name = "remote-api" transport = "http" url = "https://mcp.example.com/mcp" headers = { Authorization = "Bearer ${REMOTE_API_TOKEN}", X-Team = "platform" } ``` `headers` is rejected on a `stdio` server, header names must be valid HTTP tokens (and unique ignoring case), and values must not contain line breaks. Each preset writes them where its tool reads them (`headers` in every MCP file ai-rulez generates). Headers are not included in distributable plugin bundles: `generate --plugin` warns for each affected server and does not require header placeholders to resolve. A `command` or `args` value may use the `${PROJECT_ROOT}` placeholder, which resolves to the project root (the directory containing `.ai-rulez/`) during `ai-rulez generate`: ```toml [[mcp_servers]] name = "repo-tools" command = "node" args = ["${PROJECT_ROOT}/scripts/mcp.js"] ``` This is the portable way to pass an absolute project path to a server that requires one, without hardcoding a machine-specific path. Because it resolves to an absolute path, the generated file carrying it is machine-specific: keep the output gitignored or regenerate it per machine. In `env` values, `${PROJECT_ROOT}` also resolves this way unless a real `PROJECT_ROOT` is supplied via `--env`, the process environment, or a dotenv file, in which case that value wins. For Claude Code specifically, a project-scoped `.mcp.json` can instead use Claude's own `${CLAUDE_PROJECT_DIR:-.}` token in `command`/`args`; ai-rulez passes it through unchanged, and it stays portable across machines. MCP env and header placeholders use `${VAR}` syntax, where `VAR` must match `[A-Za-z_][A-Za-z0-9_]*`. They are resolved during `ai-rulez generate` from repeated `--env KEY=VALUE` flags, process environment variables, then dotenv files. By default, `ai-rulez` loads `.env` from the generation base directory. If any `--env-file PATH` flags are supplied, the default `.env` is not loaded; files are merged in flag order, with later files winning. Generation fails if a placeholder cannot be resolved. Generated MCP config files contain resolved values. If any resolved MCP env or header value comes from a placeholder, or if an env key or header name looks sensitive (`TOKEN`, `SECRET`, `PASSWORD`, `KEY`, `CREDENTIAL`, or the `Authorization`, `Proxy-Authorization` and `Cookie` headers), generation fails before writing unless the generated MCP config path is covered by `.gitignore` or by the patterns `ai-rulez` is about to add. The check looks at what is rendered: an MCP config file (`.mcp.json`, `.gemini/settings.json`, `.agents/mcp_config.json`, `opencode.json`, a scoped variant such as `packages/web/.mcp.json`, ...) is protected only when it holds a resolved secret value. A file that only refers to the variable (`${env:TOKEN}`) and a merged document that is not an MCP config (a review check file) need no ignore entry. Resolved secret values are redacted before source-hash calculation, but generated MCP config files contain the actual resolved values. Claude Code reads project servers from `.mcp.json`, which references `${VAR}`; `.claude/settings.json` carries no `mcpServers`, and a `mcpServers` entry an earlier version wrote there is removed on the next `generate`. **Environment references.** Where a tool expands environment references in its own MCP config, a value that came from a `${VAR}` placeholder resolved from the process environment is written as that tool's reference instead of the secret. A value from `--env`, `--env-file`, `.env`, `${PROJECT_ROOT}` or an unresolved lenient placeholder is always written resolved, and a file that holds only references is not made owner-only. | Tool (preset) | Reference | Where | Documentation | | --- | --- | --- | --- | | Claude Code and the readers of the root `.mcp.json` (`mcp`, `codebuddy`, `commandcode`, `reasonix`, `cursor`, `copilot`) | `${VAR}` | `.mcp.json` env and headers | | | Gemini CLI (`gemini`) | `${VAR}` | `.gemini/settings.json` env and headers | | | Amp (`amp`) | `${VAR}` | `.amp/settings.json` env and headers | | | Pi (`pi`) | `${VAR}` | `.pi/mcp.json` env and headers | | | Factory Droid (`factory`) | `${VAR}` | `.factory/mcp.json` env and headers (not `url`, `args`) | | | OpenCode (`opencode`), Kilo (`kilo`) | `{env:VAR}` | `environment` and `headers` | | | Cursor (`cursor`), Devin (`devin`) | `${env:VAR}` | `.cursor/mcp.json`, `.devin/mcp_config.json` | | | Codex (`codex`) | `env_vars`, `env_http_headers`, `bearer_token_env_var` | `~/.codex/config.toml` / `.codex/config.toml` | Codex configuration reference | | Codebuff (`codebuff`), Crush (`crush`) | `$VAR` (whole value only) | env and headers | tool documentation | A reference with a delimiter (`${VAR}`, `${env:VAR}`, `{env:VAR}`) may sit inside a longer value (`Bearer ${TOKEN}`); `$VAR` is written only when the value is exactly one placeholder, because `$TOKEN_v2` would read another variable. The root `.mcp.json` is one file shared by several presets, so every writer renders the same bytes. It carries `${VAR}` references only for upper-case names (CodeBuddy expands no others). `qoder` documents no expansion, so it writes the resolved value (owner-only). Alone, or with presets that do not read `.mcp.json`, the whole file stays resolved. Together with a preset that expands references (`claude`, `cursor`, `copilot`, `codebuddy`, `commandcode`, `reasonix`), the two renderings differ and `generate` fails naming both presets, rather than putting a resolved secret into a file a reference-reading tool shares; supply the secret with `--env` or `.env` to use both. Tools whose documentation promises no expansion (`junie`, `antigravity`, `zed`, `trae`, `cline`, `xum`, Claude Desktop, VS Code's `.vscode/mcp.json`) and tools that expand only after an opt-in (`kiro`: "Mcp Approved Env Vars") keep the resolved value. ###### Settings document merge behavior Files such as `.claude/settings.json`, `.mcp.json`, `.amp/settings.json`, `.gemini/settings.json`, `.agents/mcp_config.json` and `.pi/mcp.json` are **shared documents**: ai-rulez owns specific top-level keys (`mcpServers` for MCP config, `amp.anthropic.effort` for Amp) and the consumer owns everything else. Generation replaces only the owned keys and preserves every other member byte-for-byte, including the document's original indentation. A hand-formatted document (a one-line nested object, for example) is edited in place, so `clean` restores its original bytes. The manifest records what ai-rulez merged as digests, never as values, so a resolved secret in a merged server entry is not copied into `.generated-manifest.local.json`. **Important implications**: - **MCP servers are owned one by one**: ai-rulez owns the servers it writes into `mcpServers` (`mcp.` in `opencode.json`, `servers` in `.xum/mcp.jsonc`), not the whole object. A server you added by hand under a name that is not in `config.toml` survives `generate` and `clean`. A server dropped from `config.toml` is removed on the next `generate`, and only while it still holds the value ai-rulez wrote: an entry you edited is yours, stays, and is reported once. If a hand-written server has the same name as a configured one, the configured value wins on `generate` and is the one recorded. - **Hooks, permissions and managed keys**: top-level `[[hooks]]`, `[permissions]` and `[claude.settings.managed]` add `hooks`, `permissions.allow|ask|deny`, `env` and `skillOverrides` to the shared documents, owned element by element (hook groups, rules) or entry by entry (env, skill overrides). Hand-authored keys, groups and rules in the same arrays survive `generate` and `clean`. See [Hooks and permissions](settings.md). - **Plugin keys of `.claude/settings.json`**: with `[claude.settings] manage = true`, ai-rulez also owns `extraKnownMarketplaces.` and the listed `enabledPlugins.@` entries, each entry on its own; other marketplaces and plugins in those objects survive. - **Written only when there is something to contribute**: A settings document is emitted only when the config declares MCP servers (or, for Amp, a resolved effort tier; for Claude, managed plugin keys). The `gemini` and `antigravity` presets previously wrote their settings document on every run purely to self-register the ai-rulez MCP server; they no longer do, so a project with no `[[mcp_servers]]` keeps whatever is already at `.agents/mcp_config.json` untouched. The exceptions are `.gemini/settings.json` (`context.fileName`) and `opencode.json` (`instructions`), which carry the entry that loads machine-local content. - **JSONC**: A document with comments or trailing commas is merged in place: ai-rulez adds or updates only the keys it owns (the MCP servers, the Gemini `context.fileName` or OpenCode `instructions` entry) and every comment and the rest of the document stay as written. - **Taking keys back out**: `ai-rulez clean`, and `generate` after a preset or server is removed, remove the keys ai-rulez merged in, as long as they still hold the value it wrote, and keep the rest of the document; see [Settings documents shared with you](local-overrides.md#settings-documents-shared-with-you). - **Gitignore behavior**: A document still holding keys ai-rulez does not own is treated as the user's file: it is NOT added to the managed `.gitignore` block and is NOT deleted as stale. This preserves hand-authored settings such as Claude's `permissions`, `env`, `model`, and `statusLine`. A document holding only ai-rulez's own keys remains a generated artifact and is still gitignored (keeping resolved MCP secret values out of git). ##### `plugins` Plugins to install from a configured marketplace. This is the **consumer** side of the plugin config. It is distinct from the **producer** `[plugin]` block (see [Authoring Plugins](plugins.md)) that packages this project as a plugin. ```toml [[plugins]] marketplace = "official" # name of a [[marketplaces]] entry name = "my-plugin" scope = "project" # project or user; defaults to project enabled = true # defaults to true ``` !!! warning "No effect" `[[plugins]]` no longer writes `.claude/plugins.json` or `.codex/plugins.json`: Claude Code and Codex do not read them. The table is still accepted, and `generate` warns when it is set. Claude Code records installs in `.claude/settings.json` (`enabledPlugins`, `extraKnownMarketplaces`); Codex enables plugins with `[plugins."name@marketplace"] enabled = true` in `.codex/config.toml`. Use [`[claude.settings]`](#claudesettings) (`manage = true` with `enable_plugins`) for Claude Code, and keep the Codex entry yourself; ai-rulez does not write `.codex/config.toml` plugin entries because it owns that file outright. Delete the files an earlier version left behind. The Copilot `enabledPlugins` settings syntax is not shown in the vendor documentation, so it is not generated. ##### `plugin` The **producer** block: packaging metadata for a distributable plugin bundle (`generate --plugin`, `publish`, `--emit agent-plugins`). Skills, commands and agents come from the content tree; this block only names and describes the package. `name` and `version` are required. See [Authoring Plugins](plugins.md) and [Agent Plugins](agent-plugins.md). ```toml [plugin] name = "acme.tools" # required version = "1.2.0" # required, semantic version description = "Acme review tooling." runtimes = ["agent-plugins"] # default: all runtimes spec = "1.1.0" # Agent Plugins version: "1.0.0" (default) or "1.1.0" ``` | Key | Default | Meaning | | --- | --- | --- | | `name`, `version` | required | Package name and semantic version | | `display_name`, `description`, `homepage`, `repository`, `license`, `category`, `brand_color`, `icon`, `logo` | empty | Manifest metadata | | `keywords`, `tags` | empty | Search terms | | `runtimes` | all | Runtimes to build: `claude`, `cursor`, `codex`, `gemini`, `kimi`, `opencode`, `factory`, `hermes`, `agent-plugins`, `copilot` | | `spec` | `1.0.0` | Agent Plugins specification of the `agent-plugins`, `copilot` and root-layout `codex` packages: `1.0.0` or `1.1.0` | | `content_root` | content tree | Project-relative directory holding plugin-only `skills/`, `commands/` and `agents/` | | `include_domains` | none | Domains (names or globs) whose skills, commands and agents are bundled; a root item wins over a same-named domain item | | `include_evals` | `false` | Bundle each skill's `evals/` directory and the project-level `evals/` tree | Sub-tables: `[plugin.author]` (`name`, `email`, `url`), `[[plugin.mcp]]`, `[[plugin.hooks]]`, `[plugin.statusline]`, `[plugin.codex]` (`manifest` = `legacy`, `root` or `both`; `marketplace`), `[plugin.cursor]`, `[plugin.gemini]`, `[plugin.kimi]`, `[plugin.hermes]` and `[plugin.interface]`. ###### `plugin.interface` The rich UI block of the Codex and Kimi manifests (the Codex root layout writes it under `extensions["com.openai"].interface`, keys sorted). No key is required and unknown keys are rejected. | Key | Type | | --- | --- | | `display_name`, `short_description`, `long_description` | string | | `developer_name`, `category` | string | | `capabilities`, `default_prompt`, `screenshots` | list of strings | | `website_url`, `privacy_policy_url`, `terms_of_service_url` | string | | `brand_color`, `composer_icon`, `logo`, `logo_dark` | string | ##### `llms_txt` Controls the `llms.txt` written by the opt-in `llms-txt` preset and linted by `validate --strict` (`AR9P0` to `AR9P6`). The table has no effect without the preset. See [llms.txt](llms-txt.md). ```toml presets = ["llms-txt"] [llms_txt] dir = "" # default: the project root title = "Acme" # default: name summary = "Acme coding rules." # default: description full = true # also write llms-full.txt (default: false) include = ["rules", "context", "skills"] # default; also agents, commands ``` `agents` and `commands` are listed in the `Optional` section. `dir` must not be inside `.git` or the configuration directory. ##### `marketplaces` Plugin marketplaces to register. ai-rulez records these sources but does not currently emit any marketplace output for them; a `[[plugins]]` entry references a marketplace by `name`. ```toml [[marketplaces]] name = "official" source = "https://github.com/org/marketplace" # GitHub repo, git URL, local path, or URL type = "github" # github, git, local, or url ``` ##### `placement` Decides whether skills and commands are generated into `.claude/skills` (`core`, the default) or shipped only through a plugin. Absent block: nothing changes. See [Keep a skill out of `.claude/skills`](plugins.md#keep-a-skill-out-of-claudeskills). ```toml [placement] default = "core" plugin = ["domains/*"] core = ["domains/backend/python-conventions"] honor_targets = false ``` ##### `claude.settings` Opt-in ownership of `extraKnownMarketplaces.` and `enabledPlugins.@` in `.claude/settings.json`, merged like `mcpServers` (see [Settings document merge behavior](#settings-document-merge-behavior)). See [Register the marketplace in `.claude/settings.json`](plugins.md#register-the-marketplace-in-claudesettingsjson). ```toml [claude.settings] manage = true enable_plugins = ["acme-essentials"] ``` ##### `claude.skills` By default a skill is user-invocable and appears in Claude Code's `/` menu. `hide_from_menu = true` writes `user-invocable: false` on every skill that does not set the key itself, so only the model loads it. A `user-invocable` value in a skill's frontmatter always wins. See [Skill Frontmatter](skills.md#invocation-keys). ```toml [claude.skills] hide_from_menu = true ``` ##### `codex` `project_doc_max_bytes` is the limit your Codex is configured with (default 32 KiB). `generate` warns when the `AGENTS.md` files Codex concatenates exceed it, naming the largest sections; `0` turns the warning off. See [Rules: size limits](rules.md#size-limits). ```toml [codex] project_doc_max_bytes = 65536 ``` `[claude.settings.managed]` additionally owns `env.` and `skillOverrides.` entries without `manage = true`; `[[hooks]]` and `[permissions]` render the hooks and permission rules. See [Hooks and permissions](settings.md). ##### `hooks` and `permissions` Top-level `[[hooks]]` (lifecycle hooks for 36 harnesses, from `claude`, `codex`, `cursor`, `gemini` and `copilot` to `qwen`, `kiro` and the plugin-based `opencode`, `kilo`, `pi` and `amp`) and `[permissions]` (`allow`, `ask`, `deny` rules, translated for 24 harnesses with a native permission surface) are rendered into each harness's native settings file outside any plugin, merged key by key so hand-authored content survives, comments in JSONC, TOML and YAML included. See [Hooks and permissions](settings.md), [Permissions](permissions.md) and the per-harness [feature matrix](harnesses.md). `ai-rulez generate --user` renders a user config into the per-user directories each harness reads; see [User-level configuration](user-scope.md). ##### `builtins` Enables built-in domains that ship embedded in the `ai-rulez` binary. These provide opinionated rules, skills, agents, context, and commands without needing external includes. **Omit the `builtins` field entirely and no builtin content is loaded**, auto-included domains included. Auto-inclusion applies only once the field is present, so the minimum opt-in is `builtins = true` or any array value. ###### Enable all builtins ```toml builtins = true ``` ###### Disable all builtins (including auto-includes) ```toml builtins = false ``` ###### Enable specific builtins ```toml builtins = ["rust", "python", "pyo3", "security", "git-workflow", "default-commands"] ``` ###### Scope a builtin to a profile A builtin pack can be named in a profile's domain list with the `builtin:` prefix. The pack is then loaded for that profile only, instead of every profile: ```toml [profiles] backend = ["backend", "builtin:docker"] frontend = ["frontend"] ``` `backend` sees the `docker` pack; `frontend` does not. This works even when the root `builtins` field is absent or set to `false`, because a profile reference is an explicit opt-in. A pack the root `builtins` field already enables stays global — the prefix selects a pack, it does not un-globalize one. Prefixing also keeps a builtin (`builtin:rust`) from colliding with a local domain of the same name. ###### Exclude auto-included builtins `ai-governance` is auto-included whenever builtins are configured. Exclude it with `!`: ```toml builtins = ["rust", "!ai-governance"] ``` ###### Drop the agents roster from root files `agent-delegation` is auto-included, and it is what makes every root instructions file (`CLAUDE.md`, `AGENTS.md`, …) end in an `## Agents` section listing each agent's name and description. Exclude the domain to drop that section: ```toml builtins = ["!agent-delegation"] ``` The per-agent files (`.claude/agents/*.md`) are still generated, so no capability is lost — the roster is a second copy of text the agent files already carry, and the root file is the copy read on every request. On a tree with 32 agents the roster measures about 1,100 always-loaded tokens; `ai-rulez tokens` reports it as the `agents_delegation` line so you can see the figure for your own tree before deciding. ###### Exclude specific rules from a builtin domain When using the array form of `builtins`, you can exclude a single rule from a domain while keeping the rest: ```toml builtins = ["git-workflow", "!git-workflow/commit-messages"] ``` This loads all rules from `git-workflow` except the `commit-messages` rule. The syntax is `!domain/rule`. Per-rule exclusion only works with array form; `builtins = true` does not support per-rule exclusion. ###### Available Built-in Domains Run `ai-rulez builtins list` to see all available domains. **Universal** (language-agnostic): | Domain | Description | | ------------------- | ------------------------------------------------------------------ | | `ai-governance` | AI agent behavior governance (auto-included) | | `agent-delegation` | Agent delegation instructions and listing (auto-included) | | `security` | Security best practices and OWASP reference (auto-included) | | `git-workflow` | Git workflow and commit conventions (auto-included) | | `code-quality` | Code readability, error handling, and complexity (auto-included) | | `testing` | Testing conventions and best practices (auto-included) | | `token-efficiency` | Output efficiency and task automation (auto-included) | | `cicd` | CI/CD pipeline standards and GitHub workflow conventions | | `docker` | Container build, security, and deployment best practices | | `observability` | Logging, metrics, health checks, and observability standards | | `documentation` | Documentation standards and maintenance | | `polyglot-bindings` | Cross-language binding and native FFI conventions | | `default-commands` | Built-in slash commands (`/iterate`, `/parallelize`) | **Languages** (per-language conventions): `rust`, `python`, `typescript`, `go`, `java`, `ruby`, `php`, `elixir`, `csharp`, `r` **Bindings** (FFI binding conventions): `pyo3`, `napi-rs`, `magnus`, `ext-php-rs`, `rustler`, `wasm`, `jni-rs`, `extendr`, `cgo`, `vite-plus` ###### What Builtins Provide **Universal builtins** carry opinionated content for their domain, split between always-on rules (under the default split mode, written to `.claude/rules/*.md`; with `[rules] mode = "inline"`, inlined into `CLAUDE.md`) and on-demand skills (loaded only when relevant): - `ai-governance` — rules: read-before-write, minimal changes, verification before completion, systematic debugging, agent workflow, communication style, no AI signatures, explain reasoning - `security` — rules: input validation, secrets handling, least privilege. Skills: `owasp-quick-reference`, `dependency-awareness` (with per-language audit tool recommendations) - `git-workflow` — rules: conventional commits, atomic commits, branch hygiene, safe operations - `code-quality` — skills only: `code-quality-standards` (readability, complexity limits, anti-patterns, duplication, dead code), `error-handling` - `testing` — rule: tests ship with the behaviour change. Skills: `tdd-workflow`, `testing-conventions` (naming, assertions, independence, anti-patterns) - `token-efficiency` — rules: context preservation, output awareness. Skills: `task-runner`, `incremental-approach` - `agent-delegation` — context: delegation instructions and the generated agent listing - `cicd` — rules: pipeline standards, GitHub workflow conventions - `docker` — skill only: `container-standards` - `observability` — skill only: `observability-standards` - `documentation` — rules: inline docs, README standards, docs-with-code updates - `polyglot-bindings` — skills only: Rust-core/native ABI boundaries, FFI ownership, cross-language error conversion, binding parity - `default-commands` — commands: `/iterate` (implementation + review cycles) and `/parallelize` (subagent task splitting) **Language builtins** each provide a comprehensive conventions rule covering: - Target language version and edition - Linting and formatting tools (e.g., `ruff` for Python, `oxfmt`/`oxlint` for TypeScript, `clippy` for Rust) - Static analysis and type checking (e.g., `mypy --strict`, `PHPStan level 9`, `Dialyzer`) - Security/SAST tools (e.g., `bandit`, `gosec`, `cargo audit`, `bundler-audit`) - Testing framework and coverage tools with 80%+ threshold - Package manager and lockfile conventions - Benchmarking and profiling tools - Anti-patterns specific to the language **Binding builtins** provide FFI-specific conventions for Rust binding crates: - Macro usage patterns (e.g., `#[pyclass]`, `#[napi]`, `#[rustler::nif]`) - Error mapping between Rust and target language - Build and distribution workflow - Performance considerations (GIL release, scheduler safety, bundle size) - Anti-patterns (no panics, no blocking, thin wrapper principle) ###### Builtins as On-Demand Agent Skills Convention-heavy builtins no longer put their content into `CLAUDE.md`, `.claude/rules/` (or the other always-loaded surfaces). Instead they emit as **Agent Skills** — `.claude/skills//SKILL.md` files with YAML `name:` / `description:` frontmatter — that the assistant loads on demand only when the work is relevant. This keeps the always-loaded governance file small while still shipping the full conventions. Builtins that emit as skills: - **Language** domains: `rust`, `python`, `typescript`, `go`, `java`, `ruby`, `php`, `elixir`, `csharp`, `r` - **Binding** domains: `pyo3`, `napi-rs`, `magnus`, `ext-php-rs`, `rustler`, `wasm`, `jni-rs`, `extendr`, `cgo`, `vite-plus` - **`polyglot-bindings`** conventions - **`security`**: the `owasp-quick-reference` and `dependency-awareness` entries - **`code-quality`**, entirely: `code-quality-standards` and `error-handling` - **`testing`**, all but one rule: `tdd-workflow` and `testing-conventions` - **`token-efficiency`**: `task-runner` and `incremental-approach` - **`docker`** and **`observability`**, entirely: `container-standards` and `observability-standards` What stays inline is behavioural governance that has to land before the first file is read, where a skill loaded after the fact is too late: all of `ai-governance` and `git-workflow`, `security`'s `secrets-handling`, `input-validation` and `least-privilege`, `token-efficiency`'s `context-preservation` and `output-awareness`, and `testing`'s `test-alongside-code`. Guidance that only matters once you are already inside a specific activity — writing a test, writing an error path, writing a Dockerfile — is a skill, because its description can name that trigger and its body then costs nothing until it fires. Exclusions still work exactly the same for these now-skill entries. `!domain` drops a whole domain and `!domain/name` drops a single rule/skill within it (array form of `builtins` only): ```toml builtins = ["rust", "security", "!security/owasp-quick-reference"] ``` ###### Merge Priority Builtins have the **lowest** priority. Content is merged in this order: 1. **Builtins** (lowest) — embedded in binary 2. **Includes** — from git repos or local paths 3. **Project content** (highest) — in your `.ai-rulez/` directory (not to be confused with machine-local content under `.ai-rulez/local/`, see [Local overlay](#local-overlay)) If a local domain has the same name as a builtin, the local domain is used and the builtin is skipped entirely. ##### `defaults` Top-level defaults that propagate into generated outputs when individual content files do not override them. ```toml [defaults] effort = "medium" # low | medium | high | xhigh | max | inherit # Suppress agent frontmatter fields that a target tool would reject. omit_agent_fields = ["model", "tools"] [defaults.effort_by_preset] codex = "high" claude = "xhigh" amp = "max" [defaults.model_by_preset] claude = "opus" copilot = "gpt-5" cursor = "claude-3.7-sonnet" ``` **`defaults.omit_agent_fields`** suppresses named agent frontmatter fields for every preset, so an agent stays loadable where a field would be invalid — an unconfigured model or provider, or a tool name the target tool does not recognize. Recognized values are `model`, `effort`, `tools`, and `description`; an omitted field is simply not written. This trades strictness for loadability, which is usually the right call when the same agent is generated for many tools. **`defaults.effort`** sets the reasoning effort applied to every preset that supports it. Per-agent overrides (via agent frontmatter) win where the preset accepts per-agent effort; if neither is set, the field is omitted entirely. **`defaults.effort_by_preset`** lets you override `defaults.effort` for specific presets. Per-agent metadata still wins. Useful when, for example, you want Codex to reason harder than Claude on the same project. **`defaults.model_by_preset`** sets the agent `model` value per preset. Model strings are provider-specific (`opus` makes sense for Claude, `gpt-5` for Copilot) so there is no provider-neutral `defaults.model` scalar — every entry is preset-scoped. Per-agent `_model` frontmatter still wins; the legacy single-value `model` field on an agent acts as the lowest-priority fallback. Presets that do not emit a per-agent model frontmatter (`codex`, `antigravity`) ignore entries for their preset. **OpenCode models must be `provider/model`** (for example `anthropic/claude-sonnet-4-5`). OpenCode reads a bare alias such as `sonnet` as a provider with no model: it drops the agent file or fails the session with `Model not found`. When the resolved model for `opencode` is not provider-qualified, ai-rulez warns, omits `model`, and the agent inherits the session's model. To pin a model for OpenCode only, set `opencode_model` in the agent frontmatter or `[defaults.model_by_preset] opencode = "provider/model"`; either wins over the shared `model` field, so the same agent can keep `model: sonnet` for Claude. OpenCode agent `hidden` is written as a boolean, and `temperature` and `top_p` as top-level numbers; values that do not parse are omitted with a warning. **Resolution order** (per preset, per agent): 1. Per-agent `effort` in agent frontmatter (Claude, Codex, Devin, Opencode, Xum, pi — presets that support per-agent effort) 2. `defaults.effort_by_preset[]` 3. `defaults.effort` 4. Omit For models the order is: 1. Per-agent `_model` in agent frontmatter (e.g. `claude_model`, `copilot_model`) 2. `defaults.model_by_preset[]` 3. Per-agent legacy `model` field 4. Omit **Per-preset support matrix** — each preset accepts a different vocabulary, and ai-rulez maps your value to the closest tier the preset supports: | Preset | Where it's emitted | Field | Notes | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `claude` | `.claude/agents/.md` frontmatter | `effort` | Per-agent. Full vocabulary including `max`. `inherit` is not a Claude effort value and is dropped. | | `codex` | `.codex/agents/.toml` (per-agent) and `.codex/config.toml` (global default) | `model_reasoning_effort` | Per-agent override beats global `.codex/config.toml`. `max` → `high`; `inherit` dropped. | | `amp` | `.amp/settings.json` | `amp.anthropic.effort` | Global only. `xhigh` → `high`. | | `devin` | `.devin/agents/.md` frontmatter | `reasoning_effort` | Per-agent. `max` → `high`; `inherit` dropped. | | `opencode` | `.opencode/agents/.md` frontmatter | `variant` | Per-agent. A separate `variant:` key beside a plain `provider/model` (the `model#variant` form is `opencode.json` only); a `#variant` in the source model is split off. `xhigh` and `max` → `high`; `inherit` dropped. | | `xum` | `.xum/agents/.md` frontmatter | `ai.thinkingLevel` | Per-agent. `xhigh` and `max` → `high`; `inherit` dropped. | | `pi` | `.pi/agents/.md` frontmatter | `thinking` | Per-agent. Full vocabulary; `inherit` dropped. | | `cursor`, `copilot`, `gemini`, `junie`, `antigravity`, `cline` | — | — | These tools either gate effort behind UI toggles or read it from user-managed config files. ai-rulez does not emit anything for them; configure effort in the tool's own settings. | **Per-preset model matrix** — presets that emit a `model` value in their agent frontmatter: | Preset | Per-agent frontmatter key | Emitted field | | -------------- | ------------------------- | -------------------------------------------- | | `claude` | `claude_model` | `model` in `.claude/agents/.md` | | `copilot` | `copilot_model` | `model` in `.github/agents/.agent.md` | | `cursor` | `cursor_model` | `model` in `.cursor/agents/.md` | | `cline` | `cline_model` | `modelId` in `.cline/agents/.yaml` | | `junie` | `junie_model` | `model` in `.junie/agents/.md` | | `opencode` | `opencode_model` | `model` in `.opencode/agents/.md` | | `devin` | `devin_model` | `model` in `.devin/agents/.md` | | `gemini` | `gemini_model` | `model` in `.gemini/agents/.md` (Gemini) | | `xum` | `xum_model` | `ai.model` in `.xum/agents/.md` | | `pi` | `pi_model` | `model` in `.pi/agents/.md` | A bare Claude alias (`sonnet`, `opus`, `haiku`) is not a Gemini model, so `gemini` omits it with a warning and the agent inherits the session model; `gemini_model` and `defaults.model_by_preset.gemini` are written as given. ##### `rules` Controls how rules are written to generated outputs. ```toml [rules] mode = "split" # split (default) | inline [rules.mode_by_preset] claude = "inline" ``` - **`rules.mode`**: `split` (the default) writes one file per rule in the tool's native rules folder; `inline` embeds rules in the root file, moving only path-scoped rules to the folder. - **`rules.mode_by_preset`**: per-preset override that beats `rules.mode`. Keys are built-in, custom, or provider preset names. - **`rules.baz_scoped`**: `nested` (the default) writes the path-scoped rules and context of the `baz` preset to the `AGENTS.md` of the directory their globs point into; `root` keeps them in the root `AGENTS.md`. See [Baz](baz.md). Set `mode = "inline"` to restore the pre-4.22.0 output. Per-tool output, fallbacks and caveats are in [Rules and native rules folders](rules.md). ##### `okf` Settings for the opt-in `okf` preset, which keeps an [OKF](okf.md) bundle in sync with the sources: ```toml presets = ["claude", "okf"] [okf] dir = "docs/okf" # bundle directory, relative to the project root (default) include = ["rules", "context", "skills"] # kinds exported; default all of rules, context, skills, agents, commands, checks spec = "0.2" # the only OKF spec version implemented ``` `dir` must stay inside the project and outside the configuration directory and `.git`. The bundle is committed documentation, so `gitignore` never ignores it, and every file is written without a generated-by banner because OKF requires frontmatter at the start of each concept. ##### `lint` Tunes `ai-rulez validate`: severities, ignores, allow-lists, description bounds, size budgets (`[lint.budgets.]`), tolerated findings per rule (`[lint.ratchet]`, formerly `[lint.budget]` and `[lint.tolerate]`) and required frontmatter keys. ```toml [lint] fail_on = "warning" analyzers = ["security", "references"] # run only these analyzers ignore = ["AR803"] [lint.severity] AR401 = "error" [lint.ratchet] # tolerated findings per rule: AR401 may have up to 12 AR401 = 12 [lint.budgets.skill] # size budget of a content kind max_lines = 400 [lint.require_metadata] skill = ["owner"] ``` Every key, the finding codes and the exit codes are in [Strict validation](strict-validation.md). An [organization policy](policy.md) bounds `severity`, `ignore` and `security` from outside the repository: a repository can raise them, not lower them. ##### `verifiers` Deterministic, read-only repo checks, run by `ai-rulez verifiers run`. One `[[verifiers]]` table per check; the guide with semantics and examples is [Verifiers](verifiers.md). ```toml [[verifiers]] name = "node-version" description = "package.json pins Node" type = "key_equals" path = "package.json" key = "engines.node" equals = ">=20" severity = "error" ``` | Field | Applies to | Meaning | | --- | --- | --- | | `name` | all | Required. Letters, digits, `.`, `_`, `-`; unique. The merge key of the machine-local overlay | | `type` | all | Required: `file_exists`, `file_absent`, `glob_count`, `regex`, `forbid`, `key_equals`, `generated_in_sync` | | `description` | all | Free text shown by `list` and in reports | | `severity` | all | `error` (default), `warning` or `info`. Only errors, and warnings with `--strict`, fail the run | | `path` | `file_exists`, `file_absent`, `key_equals` | Project-relative path that stays inside the root | | `glob` | `glob_count`, `regex`, `forbid` | Files to select; `**`, `*`, `?`, `{a,b}` (at most 64 expanded alternatives per glob) | | `exclude` | `glob_count`, `regex`, `forbid` | Globs removed from the selection | | `pattern` | `regex`, `forbid` | RE2 expression, matched against the whole file | | `min`, `max` | `glob_count` | Inclusive, non-negative bounds on the match count; at least one is required | | `key` | `key_equals` | Dotted path; `\.` is a literal dot, `["a.b"]` quotes a segment, `[0]` or `.0` indexes a list | | `equals` | `key_equals` | Expected scalar, compared as text | | `profile` | `generated_in_sync` | A defined profile (or `default`); empty selects the default | A field that does not apply to the type is a configuration error, as is a glob that does not compile, a `key_equals` path that is not `.json`, `.yaml`, `.yml` or `.toml`, and a `profile` that is not defined. `config.local.toml` may declare `[[verifiers]]` too; entries merge by `name`. ##### `verifiers_settings` Limits and policy of `ai-rulez verifiers run` (the name differs from the array because TOML cannot use one key as both). ```toml [verifiers_settings] max_timeout_s = 300 # cap on a command predicate's timeout_s (1 to 900) max_file_bytes = 5242880 # largest file a content predicate reads (default 5 MiB) require_examples = false # AR9H6 for a spec verifier without examples warn_dead = false # AR9H5 for a when_changed that matches no file, on every run trust_exec_from = [] # includes whose verifiers may use the command predicate (also pin them in the lock) command_env = [] # extra environment variable names passed to command predicates ``` Credential-looking and proxy names in `command_env` and unknown includes in `trust_exec_from` are configuration errors. The table is pinned in the lock. See [Verifiers](verifiers.md#settings). ##### `header` Configures the style of headers in generated files. Headers provide context about ai-rulez, explain the folder structure, and instruct AI agents on proper usage. ```toml [header] style = "minimal" # Default: bare minimum header # style = "compact" # Shorter header with key information # style = "detailed" # Comprehensive header with full documentation # timestamp = true # Emit the "Generated:" line (default: false) # text = "..." # Override the generated prose (see "Custom header") # hashes = "full" # Freshness lines: "content" (default), "full", or "none" ``` The default is `minimal`. Every style — including `minimal` — carries the "DO NOT EDIT" warning and the injected `Content-Hash` freshness line (plus `Source-Hash` with `hashes = "full"`; see [`hashes`](#hashes)) that ai-rulez uses to detect whether a generated file changed since the last `generate`. Only the amount of explanatory prose differs between styles. ###### `hashes` Every generated file carries freshness lines in its header (a YAML comment inside the frontmatter for skills and agents, an HTML comment in `CLAUDE.md`). `hashes` selects which ones: | Value | Lines written | Effect | | ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------- | | `content` (default) | `Content-Hash` | Each header depends only on that file's own body. | | `full` | `Content-Hash`, `Source-Hash` | Adds the project-wide `Source-Hash`. | | `none` | neither | No hash lines at all. | `Content-Hash` is a hash of the file's own body and is stable. `Source-Hash` is a hash of the entire source set (config, every rule, skill, agent, command, MCP server), so under `full` one edit changes the `Source-Hash` line in every generated file. That is harmless for gitignored output, but when generated files are committed it makes every change to `.ai-rulez/` rewrite hundreds of files and makes concurrent branches conflict, which is why it is not the default. With `content` or `none`, editing one skill changes only that skill's output (and `AGENTS.md`/`CLAUDE.md` when the edit changes what they render). With `content` or `none`, `generate` decides whether to rewrite a file by comparing the whole rendered file to what is on disk (ignoring the `Generated:` text when `timestamp = true`), so a changed header style, custom header or config directory still re-renders. Consequently a formatter that edits generated files is undone by the next `generate`, as it would be for any committed-output check. `clean` does not read these lines in any mode. `verify --plugin` uses its own per-bundle provenance (the `.ai-rulez-generated.json` sidecar) and is independent of this setting. Switching modes rewrites every file once. Any other value is rejected by `validate`. ###### `timestamp` Headers carry no `Generated:` line by default, so generated output is byte-reproducible: the same sources generate the same bytes, output can be verified by content hash, and two files rendered from the same sources — `CLAUDE.md` and `AGENTS.md`, which are otherwise identical — cannot drift apart. Set `timestamp = true` to stamp each header with the time of the run: ```toml [header] timestamp = true ``` One run resolves the timestamp once and stamps every file it writes with that value, so sibling outputs always agree even when a large project takes more than a second to render. Set [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) to a Unix second count to pin it, keeping output reproducible with the line in place: ```bash SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) ai-rulez generate ``` A value that is not a parsable integer is ignored and the wall clock is used. ###### Header Styles **`minimal`** (default) - Only critical information - Brief "DO NOT EDIT" warning - MCP server reference - `Content-Hash` freshness line (and `Source-Hash` with `hashes = "full"`) - Best for: keeping always-loaded files small (the default) - Size: ~10 lines **`compact`** - Condensed version with essential information - Uses symbols (✗/✓) for clarity - Brief structure overview - Best for: projects that want a short structure overview - Size: ~20 lines **`detailed`** - Comprehensive explanation of ai-rulez - Complete folder organization documentation - Full AI agent instructions with MCP server promotion - Best for: projects where AI agents need thorough context - Size: ~50 lines ###### Custom header When the predefined styles do not match your environment — for example the default banner tells agents to use `npx`, but your project manages tools with `mise` — set `text` to replace the generated prose with your own. It overrides `style` entirely (the `style` value is ignored while `text` is set) and is written verbatim, wrapped in the output's comment syntax: ```toml [header] text = """ Built by the platform team. Set up with: mise run setup Regenerate with: ai-rulez generate """ ``` A few details worth knowing: - The freshness lines selected by `hashes` are still appended inside the banner, so hash-based skip detection keeps working. Do not add a closing comment marker of your own. - `timestamp = true` still does nothing to a custom header; add a `Generated:` line in `text` yourself if you want one. - Surrounding blank lines are trimmed; interior line breaks are preserved. Standard TOML multi-line escaping applies (`"""` for a literal block). ###### Header Example (detailed) ```html ``` ###### Header Example (compact) ```html ``` ###### Header Example (minimal) ```html ``` ##### `installed_skills` Named skills installed from external repositories. Skills are fetched dynamically during `ai-rulez generate` and included in outputs. See [Installed Skills](installed-skills.md) for full details. ```toml [[installed_skills]] name = "kreuzberg" source = "https://github.com/kreuzberg-dev/kreuzberg" [[installed_skills]] name = "ai-rulez" source = "https://github.com/Goldziher/ai-rulez" ref = "main" [[installed_skills]] name = "custom-lib" source = "https://github.com/org/repo" path = "libs/custom" # defaults to skills/ local_override = "../local" # use local path for development ``` Each entry supports: | Field | Required | Description | | ---------------- | -------- | ----------------------------------------------------------------- | | `name` | Yes | Unique skill identifier | | `source` | Yes | Git URL or local path | | `path` | No | Path within repo to the skill directory, or to a directory of skill directories (defaults to `skills/`) | | `ref` | No | Git ref (branch, tag, commit) | | `local_override` | No | Local path override for development | | `domain` | No | Tie the skill to a domain so profiles and roles select it by domain like local content. Omit for global. | | `include` | No | Keep only skills whose directory name matches one of these globs | | `exclude` | No | Drop skills whose directory name matches one of these globs; wins over `include` | | `name_prefix` | No | Prepend this to every resolved skill name | | `trust` | No | Scan level of the imported skills: `error` (default) or `warn` | Manage via CLI: `ai-rulez skill install/remove/list`. ##### `[skills]`, `[domains.]`, `[[skill_sources]]`, `[lock]` Dynamic skill loading (see [Dynamic skill loading](mcp-server.md#dynamic-skill-loading)). All optional. | Key | Meaning | | --- | ------- | | `[skills] delivery` | Global default delivery: `static` (default), `served` or `both`. | | `[domains.] delivery` | Default delivery of a domain's skills. A skill's `delivery` frontmatter wins. | | `[[skill_sources]]` | `name`, `url`, `ref`, `path`, `include`, `exclude`, `name_prefix`, `trust` (`error` or `warn`), `domain` (served only when the active profile or role selects it), `max_skills` (default 200), `max_bytes` (default 64 MiB), `max_clone_bytes` (default 256 MiB), `max_clone_files` (default 20000; each entry counts as at least 4 KiB toward `max_clone_bytes`; `AI_RULEZ_MAX_CLONE_FILES` sets it globally): skills served from a git repository or directory. A source over a limit is an error. | | `[lock] enforce` | The skills server refuses a served skill whose digest is not pinned in `ai-rulez.lock`. | ##### `[ard]` The [Agentic Resource Discovery](ard.md) manifest written by `publish --emit ard`. Optional. | Key | Meaning | | --- | ------- | | `publisher` | Required. Fully qualified domain name the identifiers are anchored to. | | `namespace` | Required. Identifier segment between publisher and name; colons separate sub-segments. | | `base_url` | https location of `skills//SKILL.md` (written next to `ard.json`). | | `plugin_type` | Media type of plugin entries. | | `queries` | `representativeQueries` (1 to 5 each) by resource name. | ##### `[publish]` What [`ai-rulez publish`](publish.md) ships and its policy gates. All optional; it has no credential keys. | Key | Meaning | | --- | ------- | | `runtimes` | Plugin runtimes to publish (default: the `[plugin]` runtimes). `--runtime` overrides it. | | `require_signature` | Fail unless the archive is signed (`AR9N7`). | | `require_approved` | Fail unless every item `[governance]` selects has a valid approval (`AR9N8`). | | `allow_dirty` | Publish from a tree with uncommitted changes. | | `[publish.github_release] repo` | `OWNER/REPO` of the release. | | `[publish.oci] ref` | Repository `host/path` (no tag) of the OCI target. | | `[publish.npm]` | `scope` (`@acme`, required for the npm target), `access` (`restricted` or `public`), `registry` (https). | | `[publish.marketplace.channels]` | Channel name to the git ref its pinned marketplace index points at. | | `[[publish.emitters]]` | `name` (`template`, `cursor-team-marketplace`, `port`, `aws-agent-registry`, `kiro-steering`), `template` and `output` for the template emitter, `options` (for example `blueprint` for `port`). | #### Local overlay A `config.local.toml` file beside `config.toml` is a machine-local, gitignored overlay that is merged onto this configuration in memory at load time. Use it for personal presets, profiles, MCP servers and secrets. It accepts the same keys as `config.toml` (it is validated against `schema/ai-rules-local.schema.json`; see [Schema Reference](schema.md)) and merges as follows: scalars and lists are replaced by the local value, tables merge per key, `presets` is an ordered union where `"!name"` drops a shared preset, and the named lists (`mcp_servers`, `plugins`, `includes`, `installed_skills`, `marketplaces`, `scopes`) merge by name, with `remove = true` deleting a shared entry. Unknown keys, a mismatched `version` and more than one overlay file are errors, and plugin bundles (`generate --plugin`) ignore the overlay. ```toml # .ai-rulez/config.local.toml presets = ["codex", "!cursor"] ``` The full merge table, the `ai-rulez local` commands, `--local` content under `.ai-rulez/local/` and the drift guard are documented in [Local Configuration](local-overrides.md), which is the single source of truth for this feature. #### Directory Structure ##### Root Content (Always Included) Content in these directories is always included in every generation: ```text .ai-rulez/ ├── rules/ # Mandatory rules and constraints ├── context/ # Reference documentation ├── skills/ # AI skills/prompts └── agents/ # Agent prompts ``` **Rules directory**: `rules/` - Files: `*.md` markdown files - Purpose: Mandatory constraints, standards, do's and don'ts - Included: in all generated outputs **Context directory**: `context/` - Files: `*.md` markdown files - Purpose: Reference documentation, architecture, guidelines - Included: in all generated outputs **Skills directory**: `skills/` - Structure: `skills/{skill-name}/SKILL.md` - Purpose: Specialized AI prompts and expert prompts - Included: in all generated outputs **Agents directory**: `agents/` - Files: `*.md` markdown files - Purpose: Agent prompt files for supported tools - Included: in all generated outputs ##### Domain Content (Profile-Specific) Content in domain directories is included only when that domain is in the active profile: ```text .ai-rulez/domains/ ├── backend/ │ ├── rules/ │ ├── context/ │ ├── skills/ │ └── agents/ ├── frontend/ │ ├── rules/ │ ├── context/ │ ├── skills/ │ └── agents/ └── qa/ ├── rules/ ├── context/ └── agents/ ``` Domain directories mirror the root structure: - `domains/{name}/rules/` - Domain-specific rules - `domains/{name}/context/` - Domain-specific documentation - `domains/{name}/skills/` - Domain-specific AI skills - `domains/{name}/agents/` - Domain-specific agent prompts #### File Formats ##### Markdown Files All `.md` files are treated as content. Optional YAML frontmatter is supported: ```markdown --- priority: high targets: - claude - ".cursor/rules/" custom_field: value --- # Rule or Context Title Your content here. Can include any markdown formatting. ``` ##### Frontmatter Fields **`priority`** (optional, string) - Values: `critical`, `high`, `medium`, `low`, `minimal` - Default: `medium` - Controls the order of the **Rules** and **Context** sections in generated files (higher priority first, name order breaking ties) ```yaml --- priority: critical --- ``` **`targets`** (optional, array of strings) - Selects which generated **outputs** include this content (not which source files it applies to; use `globs` / `paths` for that): preset names, root files, file paths or base names, directory prefixes (`.cursor/rules/`), globs, or `*`. Applies to rules-folder files and root files. - If empty, included in all outputs - See [Targets](rules.md#targets) for the match rules ```yaml --- targets: - "CLAUDE.md" - ".cursor/rules/*" --- ``` **`globs` / `paths`** (optional, array of strings — rules) - The files a rule applies to. The two keys are synonyms; either sets the same path scope. A comma-separated string is split into several globs (commas inside `{}` are kept). - A **path-scoped rule is not inlined into the root instructions file**. Each preset with a rules folder (`.claude/rules`, `.cursor/rules`, `.github/instructions`, ...) writes it there with native frontmatter; presets without one keep it inline under an `_Applies to: ..._` line. - `activation` (`always`, `glob`, `auto`, `manual`) and `description` select other modes. See [Rules and native rules folders](rules.md). ```yaml --- globs: - "**/*.tsx" --- ``` **`effort`** (optional, string — agents only) - Values: `low`, `medium`, `high`, `xhigh`, `max`, `inherit` - Sets the reasoning effort for a Claude Code subagent in its generated `.claude/agents/.md` frontmatter - Available levels depend on the model - Falls back to `defaults.effort` in `config.toml` when not set, then to the session-level default - Presets that don't emit effort — `cursor`, `copilot`, `gemini`, `junie`, `cline`, `antigravity` — omit it from their outputs. See the [per-preset support matrix](#defaults). ```yaml --- name: security-reviewer description: Reviews code for security regressions effort: high --- ``` **`extends`** (optional, string — agents only) - Inherits a lower-precedence agent of the given name and appends this agent's body to it - See [Extending Agents](#extending-agents) for resolution rules ```yaml --- name: code-reviewer extends: code-reviewer --- ``` **Custom fields** (optional) - Any other YAML fields are preserved and available in custom templates ```yaml --- priority: high author: engineering-team review_date: 2025-01-01 tags: [security, performance] --- ``` ##### SKILL.md Format Skills should follow this structure: ```markdown --- priority: high description: "Code reviewer expert for quality assurance" targets: ["CLAUDE.md"] --- # Code Reviewer Expert You are an expert code reviewer with deep knowledge of: - Code quality and maintainability - Testing best practices - Performance optimization ## Your Responsibilities 1. Review pull requests for correctness 2. Suggest improvements and refactoring 3. Verify test coverage ``` #### Content Merge Strategy When generating with a profile, content is merged in this order: 1. **Root rules** (`.ai-rulez/rules/`) 2. **Root context** (`.ai-rulez/context/`) 3. **Root skills** (`.ai-rulez/skills/`) 4. **Domain rules** (for each domain in profile) 5. **Domain context** (for each domain in profile) 6. **Domain skills** (for each domain in profile) Within each category, files are sorted by: 1. **Priority** (critical → high → medium → low → minimal) 2. **Filename** (alphabetical) ##### Deduplication by Name When the same rule, context, skill, agent or command **name** appears in multiple sources, the generated output includes it only **once**. Precedence (highest to lowest): 1. **Root content** (`.ai-rulez/rules/`, `.ai-rulez/context/`, etc.) 2. **Domain content** (`.ai-rulez/domains/{name}/rules/`, etc.) 3. **Include-sourced content** (`FromInclude` domains from external includes) 4. **Builtin content** (auto-included domains) Example: If both a builtin `git-workflow` domain and a local rule define `commit-messages`, the local version is used and the builtin version is dropped. Rules and context are *inlined*, so a duplicate there would render the same section twice. Skills and commands are *per-item files* (`.claude/skills/{id}/SKILL.md`), so a duplicate is worse than doubled output: both copies are written to one path and whichever lands last wins. Deduplication makes the precedence above the one that actually reaches disk. Shadowing a builtin skill with a project skill of the same name is a supported, intended pattern — it is a warning, never an error. During `ai-rulez generate` and `ai-rulez validate`, a warning is logged for each deduplicated item: ```text Duplicate rule collapsed name=commit-messages kept=.ai-rulez/rules/commit-messages.md dropped=builtin://universal/git-workflow/rules/commit-messages.md Duplicate skill collapsed name=go-conventions kept=.ai-rulez/skills/go-conventions/SKILL.md dropped=builtin://languages/go/skills/go-conventions/SKILL.md ``` ##### Name Collision Handling If a name appears in both root and a domain: ```text .ai-rulez/rules/testing.md .ai-rulez/domains/backend/rules/testing.md ``` The root version takes precedence and the domain copy is dropped — the same precedence order as above. To keep a domain-specific version, give it a name no other layer uses, or remove the root copy. ##### Output ID Collisions Deduplication resolves collisions *across* precedence layers. It cannot resolve two items in the **same** layer: there is no precedence between them. Skills and commands both render to `.claude/skills/{id}/SKILL.md`, differing only in the `user-invocable` frontmatter a command gets, so two items in one scope resolving to one id means one silently overwrites the other. `ai-rulez validate` (and `generate`) refuse instead, with the two source paths named. Two skills, or two commands, in the same directory: ```text duplicate output ids: command "deploy" (.ai-rulez/commands/deploy.md) vs command "deploy" (.ai-rulez/commands/deploy/COMMAND.md) ``` The flat form (`commands/deploy.md`) and the directory form (`commands/deploy/COMMAND.md`) resolve to the same id, so keeping both is the most common way to hit this. Delete one, or rename one side. A skill and a command sharing an id: ```text skill and command ids collide in the output namespace: skill "review" (.ai-rulez/skills/review/SKILL.md) vs command "review" (.ai-rulez/domains/qa/commands/review.md) ``` This check pools root and every domain, because the output layout has no domain segment: a skill in one domain and a command in another still land on the same path for any profile that activates both. Ids are compared case-insensitively, since a macOS or Windows checkout treats `Review/` and `review/` as one directory. Root shadowing a domain is *not* an error — it is the documented resolution above, and it applies to a command regardless of which form each side uses. #### Extending Agents Built-in and shared-module agents (`code-reviewer`, `docs-writer`, `security-auditor`, language specialists, ...) give you a solid base. When you only need to *add* project-specific guidance, don't copy the whole agent — **extend** it. ##### Agent precedence Same-named agents resolve deterministically by layer, highest precedence first: 1. **Project** — agents under your `.ai-rulez/agents/` 2. **Include** — agents pulled in from external includes 3. **Builtin** — agents shipped with ai-rulez builtin domains A higher-precedence agent replaces a lower-precedence one of the same name unless it uses `extends`. ##### Append a message with `extends` Create `.ai-rulez/agents/.md` with the same `name`, set `extends` to that name, and put your additional instructions in the body: ```markdown --- name: code-reviewer extends: code-reviewer --- Also enforce, for this repo: - No `.unwrap()`/`.expect()` in library code. - Every `unsafe` block carries a SAFETY comment. ``` The generated `code-reviewer` is the base agent's full instructions **plus** your message appended. You did not restate the base checklist — you added to it. Frontmatter fields you set win over the base; fields you omit are inherited: ```markdown --- name: docs-writer extends: docs-writer model: opus # upgrade just this agent's model effort: high # tools omitted → inherited from the base docs-writer --- When documenting the public API, include a runnable example in every supported language. ``` ##### How resolution works `extends: ` binds to the same-named (or explicitly named) agent resolved from the layers **below** the current one: - A **project** agent extends an **include** or **builtin** agent. - An **include** agent extends a **builtin** agent. - Chains compose in resolution order (builtin → include → project), across as many layers as extend one another. If no lower-layer agent of that name exists, the agent degrades to a plain agent (your body only) with the `extends` directive stripped, and `generate` logs a warning naming the missing target so a typo does not pass unnoticed. An `extends` cycle degrades the same way. ##### Extend vs. redefine - **Extend** (`extends:`) — you want the base plus a few additions. Preferred; no duplication. - **Redefine** — omit `extends` and write a complete agent to fully replace the base of that name. A higher-precedence agent without `extends` always wins over the base regardless of frontmatter. #### Configuration Examples ##### Small Project (Single Team) ```toml version = "5.0" name = "My Startup" description = "Early-stage SaaS with React + Go" presets = ["claude", "cursor"] gitignore = true ``` Directory structure: ```text .ai-rulez/ ├── config.toml ├── rules/ │ ├── code-style.md │ └── testing.md ├── context/ │ └── architecture.md └── skills/ └── code-reviewer/ └── SKILL.md ``` ##### Medium Project (Multiple Teams) ```toml version = "5.0" name = "Enterprise Platform" description = "Multi-team SaaS platform" presets = ["claude", "cursor", "gemini"] default = "full" gitignore = true [profiles] full = ["backend", "frontend", "qa", "devops"] backend = ["backend", "qa"] frontend = ["frontend", "qa"] qa = ["qa"] devops = ["devops"] ``` Directory structure: ```text .ai-rulez/ ├── config.toml ├── rules/ │ ├── general-standards.md │ └── security.md └── domains/ ├── backend/ │ ├── rules/ │ │ ├── api-design.md │ │ └── database.md │ └── context/ │ └── backend-architecture.md ├── frontend/ │ ├── rules/ │ │ ├── component-guidelines.md │ │ └── performance.md │ └── context/ │ └── design-system.md ├── qa/ │ └── rules/ │ └── testing-strategy.md └── devops/ ├── rules/ │ └── deployment.md └── context/ └── infrastructure.md ``` ##### Complex Project (Multiple Presets) ```toml version = "5.0" name = "Advanced ML Platform" description = "Research platform with team separation" presets = [ "claude", "cursor", "gemini", "devin", { name = "internal-guide", type = "markdown", path = "docs/AI_DEVELOPMENT_GUIDE.md" }, ] default = "full" gitignore = true [profiles] full = ["research", "ml-ops", "infrastructure", "frontend"] research = ["research"] ml-ops = ["ml-ops", "infrastructure"] frontend = ["frontend"] ``` #### Profile Design Patterns ##### Single Team (No Domains) For projects with a single team, skip domains entirely: ```toml version = "5.0" name = "simple-project" presets = ["claude", "cursor"] ``` ##### Multi-Team Monorepo For monorepos with multiple independent teams: ```toml version = "5.0" name = "platform" presets = ["claude", "cursor"] default = "full" [profiles] full = ["backend", "frontend", "mobile"] backend = ["backend"] frontend = ["frontend"] mobile = ["mobile"] ``` ##### Environment-Based Profiles For different behavior in dev, staging, production: ```toml version = "5.0" name = "saas-app" presets = ["claude"] default = "production" [profiles] development = ["dev-guidelines"] staging = ["staging-guidelines"] production = ["production-guidelines", "security-hardened"] ``` #### Validation `ai-rulez validate` checks the raw config file against the JSON schema (`schema/ai-rules.schema.json`) — so an unknown key or a value outside an enum is reported rather than silently dropped — and then runs the structural checks below. TOML is converted to JSON for the schema check. ```bash ai-rulez validate ai-rulez validate --config-only ``` The structural checks cover: - `version` is `"5.0"` - `name` is present and non-empty - All preset names are valid - `builtin:` references in profiles name a real builtin - Profile references resolve (a missing domain is a warning, not a failure) - File paths are valid - MCP server definitions are well-formed - A `config.local.*` overlay, if present, passes `schema/ai-rules-local.schema.json` and merges into a valid config (`--no-local` skips this) #### Programmatic Modification with CRUD Operations ai-rulez provides CRUD (Create, Read, Update, Delete) commands to programmatically modify your configuration. This is useful for: - Automation and scripting - Integration with CI/CD pipelines - Programmatic domain and rule management - Integration with AI assistants via MCP tools ##### Manual File Editing vs CRUD Commands **Manual file editing:** - Direct control over content - Use any text editor - Better for complex content - Version control friendly **CRUD commands:** - Automated directory structure creation - Frontmatter generation - Validation built-in - Easier for scripting and automation - Better for programmatic access ##### Domain Structure with CRUD When you create a domain with `ai-rulez domain add`, the following structure is automatically created: ```text .ai-rulez/domains/my-domain/ ├── rules/ # Domain-specific rules ├── context/ # Domain-specific documentation ├── skills/ # Domain-specific AI skills ├── agents/ # Domain-specific agents └── commands/ # Domain-specific commands ``` This mirrors the root structure and allows you to organize content by ownership. ##### CRUD Command Categories **Domain Management:** ```bash ai-rulez domain add # Create a domain ai-rulez domain remove # Delete a domain ai-rulez domain list # List all domains ``` **Content Management:** ```bash # Add content to root or domain ai-rulez add rule # Create a rule ai-rulez add context # Create context ai-rulez add skill # Create a skill ai-rulez add agent # Create an agent ai-rulez add command # Create a command # Remove content ai-rulez remove rule # Delete a rule ai-rulez remove context # Delete context ai-rulez remove skill # Delete a skill ai-rulez remove agent # Delete an agent ai-rulez remove command # Delete a command # List content ai-rulez list rules # List all rules ai-rulez list context # List all context ai-rulez list skills # List all skills ai-rulez list agents # List all agents ai-rulez list commands # List all commands ``` Add `--local` to any `add`, `remove` or `list` command to work on the machine-local tree `.ai-rulez/local/` instead (see [Local Configuration](local-overrides.md)). **Include Management:** ```bash ai-rulez include add # Add an include source ai-rulez include remove # Remove an include ai-rulez include list # List all includes ``` **Profile Management:** ```bash ai-rulez profile add # Create a profile ai-rulez profile remove # Delete a profile ai-rulez profile set-default # Set default profile ai-rulez profile list # List all profiles ``` `include add|remove`, `skill install|remove` and `profile add|remove|set-default` also take `--local`, which writes to the `config.local.*` overlay. ##### Frontmatter Generation When adding content via CRUD commands, frontmatter is automatically generated: ```markdown --- priority: medium targets: [] --- Your content here... ``` You can override defaults: ```bash ai-rulez add rule my-rule --priority high --targets claude,cursor ``` ##### Best Practices for Configuration Management 1. **Use CRUD for automation**: Scripts, CI/CD pipelines, and programmatic changes 2. **Use file editing for complex content**: When you need fine control over formatting 3. **Organize by domain**: Put domain-specific rules in `domains/name/` directories 4. **Validate after changes**: Run `ai-rulez validate` to check configuration 5. **Regenerate after changes**: Run `ai-rulez generate` to create tool-specific outputs 6. **Commit sources and outputs together**: `gitignore` defaults to `false`, so commit `.ai-rulez/` and the generated files; set `gitignore = true` only when the team keeps generated files out of git ##### Programmatic Workflow Example ```bash #!/bin/bash # Example: Create a new domain with rules # Create the domain ai-rulez domain add backend --description "Backend services" # Add a rule ai-rulez add rule database-standards --domain backend --priority high # Add context ai-rulez add context architecture --domain backend # Validate ai-rulez validate # Generate ai-rulez generate # Commit (generated files too, unless gitignore = true) git add .ai-rulez/ git commit -m "chore: add backend domain with database standards" ``` #### Best Practices ##### Domain Names Use names that indicate ownership or responsibility: - `backend`, `frontend`, `mobile` (service boundaries) - `api`, `database`, `queue` (technical components) - `auth`, `payments`, `search` (feature areas) Avoid: `team1`, `team2`, single letters, overly broad names ##### Organizing Content Put content in the domain that owns it. Example: - `domains/backend/rules/database-standards.md` - `domains/frontend/rules/accessibility.md` ##### Single Responsibility Each domain should represent one area: ```toml # Good: one responsibility per domain [profiles] full = ["api", "frontend", "infrastructure"] ``` ```toml # Bad: domains that bundle unrelated concerns [profiles] full = ["api-with-db", "frontend-with-build", "infrastructure-and-monitoring"] ``` ##### Document Domain Purposes Add comments to `config.toml`: ```toml # Domains: # - backend: Go services, REST APIs # - frontend: React web app # - mobile: React Native apps # - devops: Infrastructure, deployment [profiles] full = ["backend", "frontend", "mobile", "devops"] ``` #### Troubleshooting ##### "Profile not found" ```bash # Check which profiles are defined ai-rulez validate # Try generating with an explicit profile ai-rulez generate --profile backend ``` ##### "No presets specified" At least one preset is recommended. Add to config: ```toml presets = ["claude"] ``` ##### "Domain not included" Check your profile configuration: ```toml # If this profile doesn't include "backend", backend content won't appear [profiles] myprofile = ["frontend", "qa"] # backend is missing! ``` ##### Content not appearing in output 1. Verify domain is in profile: ```bash ai-rulez validate # Check profile definitions ``` 2. Check file location: - Root: `.ai-rulez/rules/`, `.ai-rulez/context/`, `.ai-rulez/skills/` - Domain: `.ai-rulez/domains/{name}/rules/`, etc. 3. Check for frontmatter errors: - Invalid YAML in `---` blocks will skip the file - Remove frontmatter to test 4. Regenerate explicitly: ```bash ai-rulez generate --profile your-profile ``` #### Next Steps - **[Getting Started](quick-start.md)**: Quick start and common patterns - **[CLI Reference](cli.md)**: All commands and flags - **[Domains & Profiles](domains.md)**: Team organization patterns ## Rules Source: https://goldziher.github.io/ai-rulez/rules/ Most AI tools can load rules from a folder, one file per rule, each with its own activation: always on, only when matching files are in play, only when the model finds the description relevant, or only when invoked by hand. ai-rulez writes your `.ai-rulez/rules/` into those folders. Why it matters: - **Path-scoped rules load only when relevant.** A rule for `**/*.tsx` costs nothing while you work on Go. - **Rules and context are separated.** The root file (`CLAUDE.md`, `.github/copilot-instructions.md`, ...) keeps context and the delegation notes; rules live in the tool's rules folder. #### Source frontmatter Activation is declared in the rule's frontmatter: ```markdown --- priority: high paths: - "src/**/*.{ts,tsx}" - "tests/**" activation: glob --- # TypeScript conventions ``` | Field | Type | Meaning | | ------------- | ---------------- | ----------------------------------------------------------------------------- | | `paths` | list or string | Files the rule applies to. `globs` is a synonym. | | `activation` | string | `always`, `glob`, `auto` or `manual`. Optional; derived when absent. | | `description` | string | What the rule is for. Required for `auto`; used by the model to decide. | `paths` and `globs` accept a YAML list or a comma-separated string (`"a/**, b/**"`). Commas inside braces (`*.{ts,tsx}`) or brackets do not split, and a backslash-escaped comma is kept. Entries are trimmed and deduplicated. | Activation | Applies | Requires | | ---------- | ------------------------------------------------ | ------------- | | `always` | in every interaction | no `paths` | | `glob` | when files matching `paths` are in play | `paths` | | `auto` | when the model judges the description relevant | `description` | | `manual` | only when explicitly invoked | nothing | `validate` rejects an unknown mode, `glob` without `paths`, `auto` without a description, and an explicit `activation: always` with `paths`. A legacy always-on activation (`trigger: always_on`, `alwaysApply: true`) with `paths` is not rejected: `validate` warns that the paths are ignored and the rule applies everywhere. ##### Resolution and legacy fields When `activation` is absent, the mode is resolved in this order (first match wins): 1. `activation` 2. Legacy Devin `trigger` (`always_on`, `glob`, `model_decision`, `manual`) and `glob` 3. Legacy Cursor `alwaysApply` (`true` is always; `false` is glob if paths are set, else auto if a description is set, else manual) 4. Derived: `glob` if `paths` are set, otherwise `always` A description alone never makes a rule `auto`. #### Output per tool The table shows the file each preset writes and the frontmatter it emits for each activation. | Tool (preset) | File | always | glob | auto | manual | | ----------------------------- | -------------------------------------------- | ------------------------------ | ------------------------------------------------- | ---------------------------- | ---------------------------- | | Claude (`claude`) | `.claude/rules/.md` | none | `paths: [...]` | always-on, warning | always-on, warning | | Cursor (`cursor`) | `.cursor/rules/.mdc` | `alwaysApply: true` | `globs: a,b` (bare), `alwaysApply: false` | `description`, `alwaysApply: false` | `alwaysApply: false` | | Devin (`devin`) | `.devin/rules/.md` | `trigger: always_on` | `trigger: glob`, `globs: "a,b"` | `trigger: model_decision`, `description` | `trigger: manual` | | Antigravity (`antigravity`) | `.agents/rules/.md` | `trigger: always_on` | `trigger: glob`, `globs: "a,b"` | `trigger: model_decision`, `description` | `trigger: manual` | | Copilot (`copilot`) | `.github/instructions/.instructions.md` | `applyTo: "**"` | `applyTo: "a,b"` | stays inline | stays inline | | Cline (`cline`) | `.clinerules/.md` | none | `paths: [...]` | always-on, warning | always-on, warning | | Junie (`junie`) | `.junie/rules/.md` | none | none; an `_Applies to: ..._` line, always loaded | always-on, warning | always-on, warning | Notes: - Cursor also writes `description` on `always` and `glob` rules when one is set. It is left off `manual` rules because Cursor treats a description without globs as agent-requested. - Cursor, Devin, Antigravity and Copilot take a comma-joined glob string, so brace patterns are expanded: `*.{ts,tsx}` becomes `*.ts,*.tsx`. - Cursor writes the `globs` line as the bare comma list its own rule files use (`globs: **/*.go,**/*.ts`), not as a quoted YAML string, because its parser is not a full YAML parser. Values outside a conservative character set keep the quotes. Devin and Antigravity parse real YAML, where a leading `*` is an alias, so they keep the quoted form (`globs: '**/*.go'`), as does Copilot's `applyTo`. - No dialect can express negated globs (`!x`), so they are dropped from the frontmatter with a warning. A rule whose globs are all negated has no scope left: where the preset has a root file (Claude, Copilot, Antigravity, Junie, ...) the rule stays inline there; in presets without one (Cursor, Devin, Cline) it is written as an always-on file and logged as a downgrade. Each such rule is warned about once per run. Inside a [scope](monorepo.md#scoped-rule-files) the negation is dropped and the scope prefix becomes the positive glob: a rule with only negated globs gets `/**`. - Junie's `_Applies to: ..._` line formats each glob as a code span, so characters such as `_` and `*` are not read as emphasis. - Context files in a rules folder are named `context-` and use the same frontmatter. - Each generated file has a generated banner after the frontmatter (see [Freshness hashes](#freshness-hashes)). Scoped (monorepo) rule placement is covered in [Monorepo](monorepo.md#scoped-rule-files). - Copilot keeps `auto` and `manual` rules in `.github/copilot-instructions.md`, because an instructions file without `applyTo` is not applied automatically. ##### Fallbacks Where a tool cannot express `auto` or `manual`, the rule is written as always-on and `generate` logs one aggregated warning naming the rules and the downgrade. Nothing is dropped. Claude, Cline and Junie have no description or manual mechanism. Copilot has no per-file equivalent for either, so those rules stay inline in its root file. ##### Freshness hashes The hash lines (`Content-Hash`, plus `Source-Hash` with `hashes = "full"`) are written inside the HTML comment banner that follows the frontmatter, never in the frontmatter, because the tools' frontmatter parsers are not documented to tolerate YAML comments. This applies to every rules-folder file, including `.mdc`. Files written by earlier versions with hashes in the frontmatter are rewritten once on the next `generate`. Hash lines follow `[header] hashes`. ##### Tools without a rules folder `gemini`, `codex`, `opencode`, `amp`, `xum`, `pi`, `baz` and `hermes` read a single root file, so rules are inlined there. The activation is kept as text under the rule heading so the scope is not lost: ```markdown ## TypeScript conventions _Applies to: `src/**/*.{ts,tsx}`, `tests/**`_ ``` `auto` rules get `_When relevant: _`. `manual` rules render as always-on and log a warning. Custom provider presets get the same text for inlined rules. #### `[rules] mode` ```toml [rules] mode = "split" # split (default) | inline [rules.mode_by_preset] claude = "inline" copilot = "split" ``` The default is `split`. For inline rules, set `mode = "inline"` globally, or opt out for single presets with `mode_by_preset`. | Mode | Rules folder receives | Root file keeps | | -------- | -------------------------------------- | ---------------------------------------- | | `split` | every rule, plus path-scoped context | context and delegation notes, no rules | | `inline` | path-scoped rules and context only | all other rules, all unscoped context | `mode_by_preset` overrides `mode` for one preset. Switching modes removes the stale files on the next `generate`. What each preset writes: | Preset | `split` | `inline` | | ------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------- | | `claude` | every rule in `.claude/rules/*.md` (`paths` when scoped) | path-scoped rules in `.claude/rules`; the rest in `CLAUDE.md` | | `copilot` | rules in `.github/instructions/*.instructions.md` (`applyTo`) | path-scoped rules only; `auto`, `manual` and negated-only-glob rules stay in `copilot-instructions.md` in both modes | | `antigravity` | every rule in `.agents/rules`; inline if `gemini` is also enabled and `mode_by_preset` does not set it | path-scoped rules in `.agents/rules` | | `junie` | every rule in `.junie/rules/*.md` | no rule files; everything in the root `AGENTS.md` | | `cursor`, `devin`, `cline` | `.cursor/rules/*.mdc`, `.devin/rules`, `.clinerules`; `mode` has no effect | same | | `gemini`, `codex`, `opencode`, `amp`, `xum`, `pi`, `hermes` | rules inline, with `_Applies to:_` / `_When relevant:_` lines | same | | `baz` | rules inline in `AGENTS.md`; path-scoped ones in the `AGENTS.md` of their directory (`rules.baz_scoped`, see [Baz](baz.md)) | same | Custom provider presets follow `mode` when their `outputs.rules` sets `split`. Cursor, Devin and Cline have no rules-bearing root file, so they always write one file per rule. Unscoped context goes to the folder as well for Cursor, Devin and Cline (the always-file presets). Root files always keep context (unscoped context in both modes) and the delegation notes. ##### Context Context stays in the root file. Only path-scoped context moves to the rules folder (`context-`), where the tool loads it for matching files. ##### Antigravity and Gemini Both presets write `GEMINI.md`, and the last writer wins. When both are enabled, Antigravity keeps all rules inline so `GEMINI.md` stays complete. Set `rules.mode_by_preset.antigravity` explicitly to override; ai-rulez then warns that rules may load twice or be missing from `GEMINI.md`. ##### Copilot - Instruction files without `applyTo` are not applied automatically on GitHub.com, so `auto` and `manual` rules are never written as files; they stay in `copilot-instructions.md`. - In `split` mode always-on rules become `applyTo: "**"` files, which Copilot applies only when it has file context. Use `inline` for Copilot if a rule must apply to chat without file context. ##### Claude and Copilot together VS Code Copilot also reads `.claude/rules`. With both presets enabled, a rule can load twice. Path-scoped rules are written to both folders in either mode; `split` makes every rule load twice. Keep one of the two presets on `inline` to limit the overlap. ##### Junie and a root `AGENTS.md` Junie looks for guidance in tiers: `.junie/AGENTS.md`, then root `AGENTS.md` together with `.junie/rules`, then the legacy `.junie/guidelines.md`. If another preset (`codex`, `opencode`, `amp`, `xum`, `pi`) writes a root `AGENTS.md`, Junie may prefer it over `.junie/guidelines.md`. Keep Junie content out of `guidelines.md` in that case, or use `split` so rules load from `.junie/rules`. #### Targets Frontmatter `targets` restricts where a rule or context file is written. It applies to every rules-folder file and to every inlined root file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.hermes.md`, `.junie/guidelines.md`, `.github/copilot-instructions.md`, and the `*.local.md` variants). Items without `targets` go everywhere. ```markdown --- targets: ["claude", ".cursor/rules/"] --- ``` A target selects an output when it is one of: | Form | Example | | --------------------------- | -------------------------------- | | preset name | `claude`, `cursor` | | root file (path or base name) | `CLAUDE.md`, `copilot-instructions.md` | | exact path | `.claude/rules/go.md` | | base name | `go.md` | | directory prefix | `.cursor/rules/` | | whole tree | `.cursor/rules/*`, `.cursor/**`, `.cursor/rules/**` | | glob (`path.Match` syntax) | `.claude/rules/*.md` | | everything | `*` | Matching rules: - A target naming a preset's root file (for example `CLAUDE.md`) also selects that preset's rule files. In `split` mode every rule goes to the rules folder, so such a rule lives in `.claude/rules/` and not in `CLAUDE.md`; in `inline` mode only path-scoped rules do. - A whole-tree target is `/*` or `/**` with a literal directory (no wildcard in ``): it matches everything below `` at any depth, exactly like `/`. Only `*` and `**` alone match everything. Any other pattern is a `path.Match` glob against the full path or the base name, where `*` does not cross `/` and `**` is no deeper than `*`; so `.*/rules/**` matches only files directly in the folder. - Paths compare case-insensitively; `\` and a leading `./` or `/` are accepted. - `AGENTS.md` is shared by `codex`, `opencode`, `amp`, `xum` and `pi`, and `GEMINI.md` by `gemini` and `antigravity`. Naming any preset that writes a shared file, or the file itself, selects it for all of them, so the file stays identical whichever preset writes it. With `agents_md = true` every preset that reads the shared `AGENTS.md` is an owner, and rules-folder presets stop inlining always-on items into their own root files; see [AGENTS.md and .agents/skills](agents-md.md). - A rule targeted only at a rules folder (for example `.junie/rules/`) is written there even in `inline` mode. Where inline mode writes no files for that folder (Junie, and providers without `inline_filter`), it is omitted. - A rule targeted only at skill or agent files appears in no root file. - A malformed glob (such as `[x`) never matches; `validate` and `generate` warn about it. !!! note "Behaviour change in 4.22.0" `targets` used to restrict only targeted sections, commands and skills. A rule with `targets: ["CLAUDE.md"]` now stops appearing in other root files and rules folders. Review rules whose `targets` omit an output they should still reach. #### File names A rule file is named ``. The id is the source name with spaces, `_` and path separators turned into `-`, every character outside `[A-Za-z0-9-]` dropped, and surrounding dashes trimmed. Case is kept (`Go-Style.md` stays `Go-Style.md`), but collisions are detected case-insensitively. A name with no ASCII letter or digit gets `rule-<8 hex>`, the first eight hex digits of the SHA-1 of the name. When two sources map to the same file name (context files carry a `context-` prefix), the source whose path sorts first keeps the id and the later one is written as `-<6 hex>`, the first six hex digits of the SHA-1 of its source path, with a warning. The result does not depend on scan order. Only if the suffixed name is taken too does `generate` fail, naming both sources. Two `[[scopes]]` whose paths produce the same file-name qualifier (compared case-insensitively) also fail; see [Monorepo](monorepo.md#scoped-rule-files). #### Local rules Rules in `.ai-rulez/local/rules` follow the same routing as shared rules. Where a preset sends rules to its rules folder, a local rule is written as `/.local` (for example `.claude/rules/my-rule.local.md`) instead of the `*.local.md` root file, so the tool loads it natively. Two local rules that collide get `-.local`. Local context, and rules a preset keeps inline, go to the file the tool loads for machine-local instructions: `CLAUDE.local.md`, `AGENTS.local.md` (xum and OpenCode, which lists it in `opencode.json`), `GEMINI.local.md` (Gemini CLI, which lists it in `.gemini/settings.json`), `AGENTS.override.md` (Codex, and Hermes with `agents_md`), a rules-folder file `ai-rulez.local.*` (Copilot, Junie, Antigravity), or nothing for Amp and Hermes without `agents_md`, which warn instead. Custom providers get no local output. The per-preset table is in [Local Configuration](local-overrides.md#generated-output). #### Scopes For `[[scopes]]`, rule files are written to the root rules folder, with the scope path as a qualifier and glob prefix. `auto` and `manual` rules keep their mode and so are not limited to the scope; `generate` warns once per scope about them. File names are `//` for Claude, Cursor and Copilot, `--` for Devin, Cline, Antigravity and Junie. See [Monorepo](monorepo.md#scoped-rule-files). #### Hand-written rule files With `gitignore = true`, generated rule files are gitignored one by one (for example `.claude/rules/x.md`), never the whole folder, so rules you write by hand in the same folder stay tracked. A hand-written file is never overwritten. Since 4.22.1, if it has the name of a generated rule, the generated rule is written as `.ai-rulez` instead (`.ai-rulez.instructions.md` for Copilot), `generate` warns, and the renamed file is recorded in the manifest and treated like any generated file. Rename or delete the hand-written file to put the rule back under its plain name; the next run removes the renamed file. If the renamed name is taken by a hand-written file too, the rule is skipped with a warning. Names matching `*.local.*` in a rules folder are reserved for [local rules](#local-rules): a hand-written file with such a name is skipped with a warning, and the local rule is not written. #### Size limits | Tool | Limit | | ------------ | -------------------------- | | Devin | 12000 characters per file | | Antigravity | 24576 characters per file | A file over the limit produces a warning. Content is never truncated; split the rule instead. Codex limits the combined `AGENTS.md` content instead of one file. It concatenates the files from the project root down to the working directory and stops at `project_doc_max_bytes` (32 KiB by default), so trailing content is dropped. ai-rulez inlines path-scoped rules into `AGENTS.md` for Codex, so with the `codex` preset `generate` and `tokens` warn when the root `AGENTS.md`, or a nested `AGENTS.md` plus the files above it, exceeds the limit. The warning gives the size and the five largest sections. Set the limit your Codex uses, or turn the warning off, with: ```toml [codex] project_doc_max_bytes = 65536 # 0 disables the warning; unset means 32768 ``` Use `compact = true`, skills, or `[[scopes]]` to shrink the file. The warning does not change the output. ## Skill Frontmatter Source: https://goldziher.github.io/ai-rulez/skills/ A skill is `.ai-rulez/skills//SKILL.md`: YAML frontmatter followed by the instructions. `generate` carries the frontmatter into each preset's skill directory **with its original types**. A nested `metadata:` map stays a map, `disable-model-invocation: true` stays a boolean, and `last_verified: 2026-10-01` stays a date. ```yaml --- name: release-notes description: Use when writing release notes for a tag. license: MIT compatibility: Needs git and gh allowed-tools: Bash(git:*) Read disable-model-invocation: true paths: - "CHANGELOG.md" metadata: owner: team-a reviewed: { by: alice, date: 2026-10-01 } tags: [docs, release] --- ``` The parsed YAML of `license`, `compatibility`, `allowed-tools`, `metadata`, `disable-model-invocation` and any other key is equal in source and output. Collections are written in block style, aliases are resolved, comments are dropped, and keys are sorted, so output is deterministic. #### What each preset writes | Key | Claude Code (`.claude/skills`) | Cursor (`.agents/skills`) | Every other preset's skill tree | | -------------------------------------------------------- | ------------------------------------- | ------------------------- | ----------------------------------------------- | | `name`, `description` | yes | yes | yes | | `delivery` (an ai-rulez key, see below) | no: removed before writing | no | no | | `license`, `compatibility`, `metadata`, `allowed-tools` | yes | yes | yes (the Agent Skills specification fields) | | `paths` (or `globs`) | yes, gates the skill to matching files | yes | no: no other tool documents a skill `paths` key | | `disable-model-invocation` | yes | yes | Codex: `agents/openai.yaml`, see below | | `user-invocable` | yes | no | no | | any other key (`model`, `context`, `hooks`, `argument-hint`, ...) | yes | no | no | "Every other preset" means Codex, Copilot, OpenCode, Gemini, Devin, Cline, Antigravity, Xum, Baz and the shared `.agents/skills` tree of `agents_md`. Keys a tool does not document are not written to its tree: some consumers reject unknown keys (Claude.ai uploads and the Skills API accept only `name`, `description`, `license`, `compatibility`, `metadata` and `allowed-tools`). Claude Code receives every key, like the `generate --plugin` bundle, which copies `SKILL.md` verbatim. Vendor facts behind the table (verified 2026-10-04): Claude Code documents `paths`, `disable-model-invocation`, `user-invocable`, `allowed-tools`, `license`, `compatibility` (up to 500 characters) and `metadata` as skill frontmatter and ignores unknown fields; the [Agent Skills specification](https://agentskills.io/specification) defines `name` (64 characters), `description` (1024 characters), `license`, `compatibility`, `metadata` (string keys and values) and `allowed-tools` (space-separated string); Cursor documents `paths` (with `globs` as a legacy fallback), `disable-model-invocation` and `metadata`. #### Invocation keys `user-invocable` and `disable-model-invocation` are decided by the author. ai-rulez never overwrites a value you set; `yes`, `no`, `on`, `off`, `1` and `0` are accepted and written as a boolean. - **Default**: a skill is user-invocable (Claude Code's default), so the key is not written and the skill appears in the `/` menu. - **Hide all skills from the menu**: `[claude.skills] hide_from_menu = true` writes `user-invocable: false` on skills that do not set the key. A skill with its own `user-invocable: true` stays visible. - **Commands** are rendered as skills and keep `user-invocable: true` unless the command sets the key. - `argument-hint` is passed through on skills again (it is a documented skill field and skills are user-invocable). The warning that called it inert was removed. - **Codex** has no `SKILL.md` key for this. `disable-model-invocation: true` writes `.agents/skills//agents/openai.yaml` with `policy: { allow_implicit_invocation: false }`, so Codex runs the skill only when it is named with `$skill`. #### Path-gated skills `paths` (or `globs`) on a skill makes Claude Code and Cursor load it only when matching files are in play. Patterns use the same syntax as rule `paths`. The other tools ignore the key. #### Dynamic loading (`delivery`) `delivery` decides how a skill reaches the agent: `static` (default) writes it into every harness skill tree, `served` leaves it out of the trees and serves it over MCP on demand, and `both` does both. It is an ai-rulez key: it is removed from the generated frontmatter and never carried into a skill tree. ```yaml --- name: db-migrations description: Plan and run database schema changes safely. Use when changing a table. delivery: served triggers: [schema change, alembic migration, add a column] --- ``` The skill's own `delivery` wins. A role's `delivery`, `[domains.] delivery` and `[skills] delivery` set the default otherwise, so a project can serve a whole domain or every skill and mark the few core skills `static`. A harness that cannot call MCP tools keeps served skills as static files (warning `AR992`), and served skills need an `[[mcp_servers]]` entry running `ai-rulez mcp --serve-skills` (warning `AR993`). See [Dynamic skill loading](mcp-server.md#dynamic-skill-loading). #### Size limits `generate` and `tokens` warn when a root instruction file exceeds a limit the tool documents. See [Rules: size limits](rules.md#size-limits). ## Checks Source: https://goldziher.github.io/ai-rulez/checks/ Checks are code-review guidelines: per-topic instructions a review tool applies to a pull or merge request. They are a content kind of their own, next to rules, context, skills, agents and commands. #### Source files ```text .ai-rulez/checks/.md .ai-rulez/domains//checks/.md ``` The file name is the check's identity and may contain only letters, digits, `.`, `_` and `-` (`ai-rulez validate` rejects anything else, since the name reaches output paths and section markers). ```md --- description: Flags common security issues severity: high tools: [Read, Grep] targets: [cursor, kilo] --- Review the diff for injection vulnerabilities, hardcoded secrets and unsafe deserialization. Report each finding with a file and line reference. ``` | Key | Meaning | | ------------- | ---------------------------------------------------------------------------------------- | | `description` | Short summary. Used as the text when the body is empty. | | `severity` | `low`, `medium`, `high` or `critical`. Any other value fails validation. | | `tools` | Tool names the check may use. Only Amp has a place for them. | | `targets` | Presets the check applies to, like every other content kind. Default: every preset. | At render time the names are checked again (includes bring checks that never went through validation): a name outside `[A-Za-z0-9._-]` is skipped with a warning, and two names that differ only in case count as one check (their files would collide on a case-insensitive file system); the one from the higher-precedence source is kept. Checks follow profiles like other content: root checks are always included, domain checks when the domain is in the active profile. A root check shadows a domain check of the same name. #### Managing checks ```bash ai-rulez add check security --description "Security issues" ai-rulez add check perf --domain backend ai-rulez list checks [--domain backend] [--format json] ai-rulez remove check security [--domain backend] [--yes] ``` The MCP server exposes `create_check`, `read_check`, `update_check`, `delete_check` and `list_checks`. An include can import them with `include = ["checks"]`. `create_check` and `update_check` write the frontmatter with a YAML encoder, so a description such as `Flags: injection # not a comment` is quoted correctly, and they validate `severity` and `targets` (a preset name, `*`, or a path or glob such as `src/**`; a misspelt preset is rejected instead of selecting nothing). `update_check` needs `content`, at least one field (`description`, `severity`, `tools`, `targets`), or both, and rejects an empty update. Content without frontmatter replaces the body and keeps the check's own frontmatter, comments and unknown keys; fields set on it; content with its own frontmatter replaces the file (the fields are still applied over it). #### Where they are written | Preset | Output | Shape | | ------------ | ----------------------------------------------- | --------------------------------------------------- | | `cursor` | `.cursor/BUGBOT.md` | one block, one section per check (Bugbot) | | `kilo` | `REVIEW.md` | one block, one section per check | | `qwen` | `.qwen/review-rules.md` | one block, one section per check | | `factory` | `.factory/skills/review-guidelines/SKILL.md` | one block in a skill with `name`/`description` frontmatter | | `rovodev` | `.rovodev/.review-agent.md` | one block, one section per check | | `amp` | `.agents/checks/.md` | one file per check | | `augment` | `.augment/code_review_guidelines.yaml` | merged into the hand-written YAML | | `gitlab-duo` | `.gitlab/duo/mr-review-instructions.yaml` | merged into the hand-written YAML | The aggregate files are shared with you. ai-rulez writes only a marker-delimited block: ```md ## security Review the diff for injection vulnerabilities... ``` Everything outside the two marker lines is yours and is kept: a hand-written `REVIEW.md` gets the block appended after a blank line, text above and below an existing block survives every `generate`, and `clean` (or removing the last check) takes back only the block. A file ai-rulez created that holds nothing else is deleted with its block; one that holds your text is never deleted, never git-ignored, and `generate` fails with an error rather than touch a file whose markers do not pair up. Do not edit inside the block (the next `generate` rewrites it) or put the marker lines in a check's text. For `factory` the `name`/`description` frontmatter is written once, when the file is created. Each check is a marker line ``, a `## ` heading and the check text (the description when the text is empty). They take no other metadata: severity and tools have no equivalent in those tools. The Cursor file is not written by `generate --user`: no per-user Bugbot file is documented. Amp files carry `name`, `description`, `severity-default` (from `severity`) and `tools` in the frontmatter. Augment gets one area per check, keyed by the check name, with the text as the rule description and `globs: ["**"]`. Augment's severity scale stops at `high`, so `critical` is written as `high` and an unset severity as `medium`. GitLab Duo gets one `instructions` group per check (`name`, `instructions`), with no file filter. Both YAML files are documented as hand-written, so ai-rulez merges: it owns only the areas or groups it renders (matched by name), keeps everything else, including `file_paths_to_ignore` and any other key, and removes its own entries when a check disappears. An area or group you wrote under a check's name is yours: it is never replaced or removed, the check of that name is not written there, and `generate` warns (rename one of them). An entry ai-rulez wrote earlier is updated while it still equals what was written; once you edit it, it is yours too. Comments, key order and quoting of the groups and areas you wrote survive: an unchanged GitLab group keeps its source text byte for byte, and only changed or new groups are re-rendered (an area ai-rulez writes is rendered fresh). Not generated: Augment area grouping and per-area globs, GitLab `fileFilters`, Takt quality gates, Hermes pre-verify specs, and JetBrains AI Assistant (its self-review path is a per-user IDE setting). #### Notes per tool - Kilo reads `REVIEW.md` from the pull request base branch, only once **Use REVIEW.md** is on in the Kilo web app, and truncates it at 10,000 characters. `generate` warns when the file (your text and the block) is longer. - Qwen Code `/review` and Bugbot also read these files from the base branch. Bugbot additionally reads nested `/.cursor/BUGBOT.md` files, which ai-rulez does not generate. - A hosted reviewer only sees files that are committed. Check outputs are never added to the managed `.gitignore` block, even with `gitignore = true`, so commit them. An entry an earlier version added is removed on the next `generate`. - The Amp format is taken from the rulesync documentation; Amp's own manual does not describe it, so verify it against your Amp version. - A skill named `review-guidelines` collides with the Factory output; `generate` fails with an error until one of them is renamed. - Machine-local checks (`.ai-rulez/local/checks/`) get a gitignored file of their own only for presets that write one file per check (Amp); the shared aggregate and YAML files never take a local check, and a warning names the presets that could not write it. A local check may not share a name with a shared one. - `ai-rulez doctor` and `ai-rulez tokens` include the check files; the token report lists them as surface whose loading ai-rulez does not model. ## OKF (Open Knowledge Format) Source: https://goldziher.github.io/ai-rulez/okf/ ai-rulez can export its rules, context, skills, agents and commands as an [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) bundle, import an existing OKF bundle into `.ai-rulez/`, keep a bundle in sync through the opt-in `okf` preset, and lint any bundle with `ai-rulez okf validate`. OKF is a directory of markdown files with YAML frontmatter, meant to be read by people and by agents. ai-rulez implements **OKF spec v0.2** (`okf_spec = "0.2"`). The reader is tolerant, the writer is strict. #### Sources and what was verified Research was done on 2026-10-05. | Source | URL | Status | | --- | --- | --- | | Normative spec | `GoogleCloudPlatform/knowledge-catalog`, `okf/SPEC.md` | Read in full. Pinned at repo commit `58e16bdb7a34430f055ea57e84655cff37000c03`; the file last changed in `62432a095456147ee71e70ac6e4dc0d2dea3ac30` (2026-08-21, "every timestamp is an ISO 8601 datetime with an explicit offset") | | Spec, new home | `GoogleCloudPlatform/open-knowledge-format`, `SPEC.md` | Byte-identical to the above at commit `ad30107c31c06aec8a7d5636e0d1058118604e6f`; the ecosystem map says the older `knowledge-catalog/okf` copy is frozen | | Reference bundles | `open-knowledge-format/bundles/{acme_retail,ga4,stackoverflow,crypto_bitcoin}` | Read `acme_retail`; a trimmed copy is vendored as a test fixture (see [Fixtures](#fixtures-and-licences)) | | Third-party site | (`/`, `/spec/`, `/quickstart/`, `/validator/`, `/tools/`, `/ecosystem-map/`, `/skill/`, `/faq/`) | Read. It is a guide around the spec, not the spec | | Skill repo | (`skills/okf-open-knowledge-format`) | Read the listing; its `validate.sh` (E1-E4, W1-W7) is the only "validator" with exit codes | | Candidate Go implementations | `cwest/okfctl`, `openknowledge-sh/openknowledge` | Evaluated, see [Build or borrow](#build-or-borrow) | ##### Spec facts (verified against SPEC.md v0.2) - A bundle is a directory tree of UTF-8 markdown files. A concept is any `.md` file other than the reserved `index.md` and `log.md`; its ID is the path minus `.md`. Directory layout is the producer's choice. - Frontmatter: only `type` is REQUIRED and must be a non-empty string. Recommended: `title`, `description`, `resource`, `tags`. v0.2 adds the optional families `sources`, `generated`, `verified`, `status` (`draft`, `stable`, `deprecated`), `stale_after`, and the `Attested Computation` type with `runtime`, `parameters`, `computation`, `executor`, `attester`. All timestamps are ISO 8601 with an explicit UTC offset. `timestamp` and the body `# Citations` list are v0.1 leftovers. - Type values are free text, not registered. Consumers MUST tolerate unknown types. Unknown extra keys MUST be preserved on round trip and MUST NOT cause rejection. - Links are standard markdown links, either bundle-relative (`/tables/x.md`, recommended) or relative (`./x.md`). Consumers MUST tolerate broken links. - `index.md` may appear in any directory. It has NO frontmatter, except that the bundle-root one MAY carry `okf_version`. Body: headings, each followed by bullets `* [Title](url) - description`. Subdirectory entries use `subdir/` in the spec text and `subdir/index.md` in the official `acme_retail` bundle; both occur. - `log.md` is optional: `## YYYY-MM-DD` headings, newest first, prose bullets. - Conformance is exactly three rules: every non-reserved `.md` has parseable frontmatter; every frontmatter has a non-empty `type`; present `index.md` and `log.md` follow their structure. A consumer MUST NOT reject a bundle for missing optional fields, unknown types, unknown keys, broken links or missing index files. - Versioning is `.`, declared as `okf_version: "0.2"` in the root `index.md`. Consumers that do not know the version should read best-effort. ##### Where okf.md and the spec disagree The marketing site describes a different shape than the normative spec. ai-rulez writes the spec's shape by default, writes the site's `index.md` shape with `index_style = "frontmatter"` (see [Index styles](#index-styles)), and reads both. | okf.md home page claims | Normative SPEC.md v0.2 | | --- | --- | | "Three rules": index.md per bundle, typed frontmatter, git-native history | Three rules: parseable frontmatter, non-empty `type`, reserved files well formed. `index.md` is optional. Git is a recommendation, not a rule | | `index.md` frontmatter has `title`, `version` (semver `0.1.0`) and `entries` | `index.md` has no frontmatter except `okf_version` at the root; the listing is the body. Both are accepted on read; `index_style` picks the one written | | `type` is one of `concept`, `howto`, `reference`, `decision`, `metric` | Free text; the examples are `Metric`, `Playbook`, `Reference`, `BigQuery Table`, `Attested Computation` | | "semantic versioning protecting the investment" | `.` only | | Browser validator, "coming soon" | No validator is specified | | Footer: MIT | The spec repo is Apache-2.0; the `skills` repo is Apache-2.0 too | The site's `validate.sh` (in `fabricioctelles/skills`) exits with its error count, and treats a non-directory argument as exit 1. It checks E1 no frontmatter, E2 empty `type`, E3 frontmatter in a nested `index.md`, E4 an Attested Computation without `runtime`; warnings W1 (no title or description), W3 (legacy `timestamp`), W5 (`log.md` without ISO headings), W6, W7 (stale), and an unknown `status`. ##### Assumed or uncertain - The spec is young (v0.1 on 2026-06-12, v0.2 on 2026-07-24), pre-1.0, and single vendor. Fields may still be renamed. ai-rulez pins 0.2 and treats any other declared version as best-effort (AR9B3, info). - Whether `index.md` entries must list every concept is **not** required by the spec ("entries SHOULD include the description"). AR9B0 is therefore a warning, not an error. - `x-ai-rulez` as an extension key is allowed by section 4.1 (any extra key), but no OKF validator is known to reject it; the Rust and Go linters listed on okf.md check `type` and reserved files only. Not tested against every third-party linter. - Whether other tools preserve unknown keys on their own round trip is unverified. #### Build or borrow ai-rulez needs a small bundle model, a conformance validator and an index writer. Both Go candidates are Apache-2.0, as is the spec; ai-rulez is MIT, so copying would need attribution notices. | Criterion | `cwest/okfctl` (a0b5072) | `openknowledge-sh/openknowledge` (1d6b0e4) | | --- | --- | --- | | License | Apache-2.0, copyright Google LLC headers | Apache-2.0 | | Usable as a library | No: everything is `internal/`, Cobra CLI | No: module `packages/cli`, everything `internal/` | | Spec correctness | Floor matches v0.2 (parseable frontmatter, non-empty `type`, index frontmatter rules incl. the `okf_version` carve-out). Puts `okf_version` in a `.okf` sidecar and scaffolds the index without it | Not audited in depth; built around a claims/RDF model, not the bare spec | | Dependencies | cobra, yaml.v3, goldmark + goldmark-meta (small) | mangle-go, goRDFlib, antlr, jsonschema, fsnotify, flock, x/term, and a `telemetry` package | | Size of the part we need | `internal/okf` is ~7.2k lines for authoring, search, eval, migrate, promote; the conformance floor (`validate.go`, `frontmatter.go`, `reserved.go`) is ~330 lines | Large, not separable | | Tests | 60 test files in `internal/okf`, five fixture bundles in `testdata/` | 109 test files | | Fit for ai-rulez | Different index shape (tags suffix, shape text); API built around its own Bundle and Node | Telemetry and heavy deps rule it out | **Decision: (c) write our own against SPEC.md, borrow only naming.** The floor is about 300 lines in Go with `yaml.v3`, which ai-rulez already uses, so copying would buy no maintained code and would add NOTICE obligations and okfctl's index conventions. We reuse okfctl's *check vocabulary* so its users recognize the codes (table below) and we rebuilt its five fixture *cases* (bad-frontmatter, empty-type, no-type, unknown-type, good-bundle) as our own tiny fixtures. Nothing is copied, so no NOTICE entry is needed. openknowledge is not used (telemetry, dependencies). | okfctl check | ai-rulez code | | --- | --- | | `orphan` | AR9B4 `okf-orphan` | | `broken-link` | AR9B2 `okf-link-broken` | | `spec-version` | AR9B3 `okf-version-invalid` | | `type-hygiene` / missing type / unparseable frontmatter (floor) | AR9B1 `okf-type-invalid` | | index shape and `index check` drift | AR9B0 `okf-index-mismatch` | | reserved-file frontmatter / log headings (floor) | AR9B6 `okf-reserved-structure` | | (none) export drift | AR9B5 `okf-export-drift` | #### Mapping ai-rulez concepts become OKF concepts. The real identity lives in the `x-ai-rulez` extension key, so a round trip is lossless; the OKF `type` is only for OKF readers. Names round trip through `x-ai-rulez.id`, not through the file name. A name that is not safe in a bundle path (spaces, accents, CJK) is written to a transliterated path (`résumé` becomes `r-sum.md`) and restored from the id on import. A rule, context or other concept named `index` or `log` is stored as `index_.md` / `log_.md`, because those names are reserved; the generated `index.md` is never overwritten. An id containing a path separator, `..`, a control character or a leading dot is ignored and the name is derived from the path, with a finding. An `x-ai-rulez.domain` that is not a safe name is reported and the concept imports at the project root. Index descriptions are markdown-escaped (`[`, `]`, `<`, `>`). Layout of an exported bundle (`docs/okf/` by default): ```text index.md okf_version + one section per kind rules/index.md rules/.md context/.md skills//SKILL.md + references/, scripts/, assets/ next to it agents/.md commands/.md checks/.md domains///... same shape for domain content ``` | ai-rulez | OKF `type` written | Notes | | --- | --- | --- | | rule | `Decision` | A rule states a decision the team made. Override by setting `type` in the item's `okf:` metadata map (for example `okf:` with `type: Policy` nested under it); an import stores a foreign `type` there | | context | `Concept` | Background knowledge | | skill | `Playbook` | The SKILL.md body is the procedure; resources ship next to it | | agent | `Reference` | OKF has no agent type. `x-ai-rulez.kind: agent` restores it | | command | `Reference` | Same, `kind: command` | | check | `Reference` | Same, `kind: check` | | skill resource (markdown) | `Reference` | Wrapped in frontmatter so the bundle stays conformant; stripped on import. Non-markdown resources are copied verbatim | | metric | n/a | ai-rulez has no metric concept; an imported `Metric` becomes context | Nothing is skipped silently: agents and commands are exported as `Reference` and only left out when `--include` / `include` excludes them, and the command prints what it wrote per kind. Frontmatter written for every concept: ```yaml --- type: Decision title: Testing description: One line taken from the item's description x-ai-rulez: kind: rule # rule | context | skill | agent | command | skill-resource id: testing domain: backend # only for domain content metadata: # priority, targets, globs, paths, owner, version, ... with their YAML types priority: high globs: ["**/*_test.go"] --- ``` No timestamps, no `generated.by` (it would change with the tool version). History is git's job. `index.md` entries are sorted and each carries the description. ##### Import mapping `type` (case-insensitive, `-`/`_`/space ignored) decides the target when the concept has no `x-ai-rulez.kind`: | OKF type | ai-rulez | | --- | --- | | `decision`, `rule`, `convention`, `policy`, `guideline`, `standard` | rule | | `howto`, `playbook`, `runbook`, `procedure`, `skill` | skill | | `concept`, `reference`, `metric`, anything else (incl. `Attested Computation`) | context | `--into rules|context|skills` forces every concept to that kind. Without `--into`, a concept whose `x-ai-rulez.kind` is `agent`, `command` or `check` is imported as that kind, so a bundle can also create agents, commands and checks (the security scan runs on all of them first). OKF keys with no ai-rulez equivalent (`tags`, `resource`, `sources`, `status`, `stale_after`, ...) are kept as extra frontmatter so they survive a later export. #### Codes See [strict validation](strict-validation.md). AR9B0-AR9B9 are reserved for OKF. | Code | Name | Default | Finds | | --- | --- | --- | --- | | AR9B0 | `okf-index-mismatch` | warning | An `index.md` entry points at a missing file, or a directory with an `index.md` has a concept or subdirectory it does not list | | AR9B1 | `okf-type-invalid` | error | Unparseable frontmatter, or `type` missing or empty (conformance rules 1 and 2). Unknown type *values* are allowed by the spec and not reported | | AR9B2 | `okf-link-broken` | warning | A relative or bundle-relative markdown link whose target is not in the bundle (the spec tolerates these, so never an error by default) | | AR9B3 | `okf-version-invalid` | warning | Root `okf_version` is not `MAJOR.MINOR`; info when it is well formed but not `0.2`, and info when the root index uses the frontmatter style (the scheme OKF 0.2 does not describe); error from `okf validate` alone when the directory has no root `index.md` naming `okf_version` (it is not a bundle, and `import okf` refuses it too) | | AR9B4 | `okf-orphan` | info | A concept reachable from no index entry and no link (only checked when the bundle has an index) | | AR9B5 | `okf-export-drift` | error | The configured bundle differs from what `export okf` would write now (project lint only) | | AR9B6 | `okf-reserved-structure` | error | Frontmatter in a nested `index.md` that is not frontmatter style, keys other than `okf_version` in a body-style root one (or other than `title`, `version`, `entries` in a frontmatter-style one), or `log.md` headings that are not ISO dates | | AR9B7 | `okf-title-duplicate` | info | Two concepts in one directory share a title | | AR9B8 | `okf-path-unsafe` | error | A symlink, a path escaping the bundle, a markdown file over the size limit that was skipped, or two paths differing only in case. `import okf` skips symlinks (warning) and refuses the other two | | AR9B9 | `okf-lossy-mapping` | info | Reported by `import okf`: a concept carries `x-ai-rulez` data this version cannot map (unknown `kind`, unsafe `id` or resource path) and is imported by its `type` instead, or a link in a concept points at a file that was not imported (left as written) | #### Format details What an export writes, so a bundle can be read without ai-rulez. ```text docs/okf/ index.md ---\nokf_version: "0.2"\n--- then "# Subdirectories" entries rules/index.md "# Concepts" entries: * [Title](file.md) - description rules/.md context/.md skills/index.md skills//index.md skills//SKILL.md type: Playbook skills//references/*.md wrapped as type: Reference; scripts/ and assets/ copied verbatim agents/.md commands/.md checks/.md type: Reference domains///... ``` - Every directory with a concept gets an `index.md` (SPEC section 8): no frontmatter except `okf_version` at the root, entries sorted by path, each with the concept's one-line description. Subdirectories are listed as `[name](name/index.md)`, the shape used by the official `acme_retail` bundle. - The `index.md` shape depends on `index_style`, see [Index styles](#index-styles). Concept files are identical in both. - No `log.md` is written: history is git's, and a log would need timestamps that break determinism. - `title` is the item id in words (`code-style` becomes "Code Style") unless an `okf.title` is kept (see below). No `generated`, `verified`, `sources` or `timestamp` fields are written, for the same reason. - `x-ai-rulez` holds `kind`, `id`, optionally `domain`, and `metadata`: every frontmatter key of the source item (`priority`, `targets`, `globs`, `paths`, `tools`, `owner`, `version`, custom keys) with its YAML type, except `description`, which becomes the OKF `description`. The body is copied byte for byte. - A skill resource in markdown gets a small wrapper (`type: Reference`, `x-ai-rulez.kind: skill-resource`, the original path) so it stays a conformant concept; a resource called `index.md` or `log.md` is stored as `index_.md` / `log_.md`, because those names are reserved. The wrapper is removed on import. - Executable bits of scripts are kept. ##### Index styles Two `index.md` schemes are in circulation. ai-rulez writes either and reads both; only `index.md` files (root and nested) differ, every concept file is byte-identical. | Style | `index_style` | Shape | | --- | --- | --- | | Body (default) | `"body"` | The OKF 0.2 scheme: no frontmatter except `okf_version` at the root; `# Concepts` and `# Subdirectories` headings with `* [Title](file.md) - description` bullets | | Frontmatter | `"frontmatter"` | `title`, `version: 0.1.0` and `entries` (a list of `title`, `path`, `description`) in the frontmatter, no body. The root also keeps `okf_version` so a spec reader still finds the version | ```yaml --- okf_version: "0.2" title: Index version: 0.1.0 entries: - title: rules path: rules/index.md description: Rules exported from ai-rulez --- ``` Set it with `[okf] index_style` or `export okf --index-style` (the flag wins). `generate --check` and `export okf --check` compare the configured style, so a bundle in the other style is drift. `okf validate` and `import okf` accept both and report the detected style (`index style:` in text output, `index_style` in JSON). A frontmatter-style root index is reported as AR9B3 info, unless the project configured that style. `import okf` ignores indexes, so either style yields the same `.ai-rulez/` tree. The default stays `body` until one scheme is clearly adopted; a change would be announced in the changelog. ##### Importing through `convert` `ai-rulez convert --from okf` runs the same mapping as `import okf` and writes the same files, but through convert's plan: the lossiness report (the bundle's findings become report findings), the security scan before anything is written (a finding names the bundle file), `--domain`, `--merge` and an atomic write. A bundle is found at the source root or in `docs/okf`. See [convert](cli.md#ai-rulez-convert). ##### Links Links inside concept bodies follow the files they point at. - `import okf`: a bundle-absolute (`/tables/orders.md`) or relative (`./orders.md`, `../x.md`) link between imported concepts becomes the relative path of the created file (`../context/tables-orders.md`), with `#fragment` and `?query` kept. Resources next to a skill are mapped the same way. A link to a file that was not imported (a skipped or non-markdown file, a missing target, an `index.md`) is left as written and reported as AR9B9 with its line. - `export okf`: a relative link between exported items (`../context/architecture.md`) becomes a bundle-absolute link (`/context/architecture.md`). A relative link to anything outside the export (a repository file, an item excluded by `--include`, a skipped builtin domain) is left unchanged and printed as a `note:`. Bundle-absolute and external links are never touched. - Links in fenced code blocks and inline code are never rewritten. Only inline `[text](target)` links and images are; reference-style definitions (`[id]: target`) and autolinks are left alone. - Link spelling is normalized: a foreign `./x.md` or `../x.md` comes back as `/kind/x.md` after an export. Every link keeps resolving to the same concept, and `export`, `import`, `export` is byte-identical. ##### Titles and conflicts `title` is the item id in words unless the item carries `okf.title`. An import keeps a bundle title that differs from the derived one as `okf.title`, so an edited title survives `export`, edit, `import --force`, `export`. When both sides changed a title, the rule is deterministic and never silent: - `import okf` never overwrites a source file that differs; without `--force` it reports a conflict (exit 2), with `--force` the bundle wins. - `export okf` and `generate` write the sources' title, so a title edited only in the bundle is drift. `generate --check`, `export okf --check` and `validate` report it as AR9B5, naming the edited title and both ways out (`generate` restores the source, `import okf --force` adopts the edit). A title edit on a markdown skill resource is not kept: its wrapper is rebuilt from the file name. ##### Keeping foreign OKF keys OKF keys ai-rulez has no field for (`tags`, `resource`, `sources`, `generated`, `verified`, `status`, `stale_after`, `timestamp`, a custom `type` or `title`, anything else) are stored on import under one extra frontmatter key, `okf`: ```yaml --- description: We use Go okf: tags: [lang] status: stable --- ``` `export okf` lifts that map back to the top level of the concept, so a bundle imported and exported again keeps its keys and its `type`. You can use the same mechanism by hand: `okf: {type: Playbook}` on a rule overrides the `Decision` type an export would write. ##### Round trip `export`, `import` and `export` again is byte-identical for the kinds ai-rulez owns (rules, context, skills with resources, agents, commands, checks, domains). The tests check this, and that a foreign bundle (the official `acme_retail` example) imports, exports and still validates. What does not survive: key order and comments inside a source file's frontmatter (the export normalizes them), and a source file that is empty after its frontmatter. ##### The native tree `ai-rulez migrate okf` turns `.ai-rulez/` itself into a bundle: the frontmatter of every concept moves under `x-ai-rulez.metadata`, `type` and `title` are added, and each directory gets an `index.md` (the root one names `okf_version`). The loader maps `x-ai-rulez.metadata` back onto the native fields, treats `type`, `title` and `x-ai-rulez` as reserved, and ignores generated `index.md`/`log.md` listings, so generated output does not change. `validate` runs `okf validate` on such a tree. `init`, `add`, `remove` and the MCP CRUD tools write this form: a new concept gets `type`, `title` and `x-ai-rulez`, and the `index.md` files are refreshed when the tree is a bundle (it has a root `index.md`); `export okf` of such a tree reproduces it byte for byte. A tree without a root `index.md` is the legacy layout: it still loads, with a deprecation notice from `validate`, `generate` and `list`. See [Migrating to v5](migration-v5.md#okf-is-the-format-of-ai-rulez). ##### What `migrate okf` will and will not touch - **Reserved names.** OKF reserves `index.md` and `log.md` for generated listings. If a rule, context file, agent or other item of yours has one of those names (`rules/index.md`, `context/log.md`), the migration refuses before it writes anything, names the files and exits 1. Rename them (for example to `index-notes.md`) and run it again; the migration never moves or overwrites such a file, so a refused run leaves the tree exactly as it was and can be repeated. - **Backup and atomic writes.** Every file is written atomically (a temporary file renamed over the target, mode kept), after the originals of the files it rewrites were copied to `.bak-okf-/` (same relative paths; reported in the summary and as `backup_dir` in JSON). A run with nothing to rewrite takes no backup. `--dry-run` and `--check` write nothing. - **Symlinks.** A symlinked rule, context file or directory is never followed or rewritten. It is listed as `[skipped]` with the number in the summary, and the run exits 1 (also for a file whose frontmatter does not parse), because the file needs your hand: replace the link with the real file, or migrate its target. A `--dry-run` reports skipped files but exits 0; `--check` exits 2 when files still need migrating, else 1 when files were skipped. #### CLI See [CLI commands](cli.md#okf-commands) for every flag. | Command | Does | | --- | --- | | `ai-rulez export okf [--output-dir dir] [--profile p \| --role r] [--include kinds] [--index-style body\|frontmatter] [--check]` | Write (or compare) the bundle. `--role` exports the slice of content a [role](roles.md) selects (domains, per-kind selectors, `extends`, checks included), the same slice `generate --role` renders; it excludes `--profile` | | `ai-rulez import okf [--into kind] [--domain d] [--dry-run] [--force]` | Bundle to `.ai-rulez/` sources | | `ai-rulez migrate okf [--dry-run] [--check]` | Convert `.ai-rulez/` in place to an OKF bundle (idempotent) | | `ai-rulez okf validate [--format json] [--fail-on sev]` | Lint any bundle | | `ai-rulez generate` / `generate --check` | Write / compare the bundle when the `okf` preset is on | | `ai-rulez validate`, `ai-rulez doctor` | Report `AR9B*` findings for the configured bundle | `--into` takes `rules`, `context` or `skills`; use `--domain` to place the result in a domain. (The design note asked for `--into domain|...`; a separate flag keeps the two choices independent.) Import sources: a local directory, or `https://host/org/repo[.git][@ref][#subdir]`, `git@host:org/repo[.git][@ref]`, `file:///path/repo[@ref]`. `ref` is a branch, tag or commit. Plain `http://` is refused. Git runs without hooks, prompts or submodules and times out after three minutes. #### Configuration ```toml presets = ["claude", "okf"] # opt in; without it only the commands above exist [okf] dir = "docs/okf" # default include = ["rules", "context", "skills"] # default: all six kinds spec = "0.2" # the only accepted value index_style = "body" # or "frontmatter", see Index styles ``` The preset exports the profile `generate` runs with, from the shared sources only (never `.ai-rulez/local/`). Domains that come from builtins or includes are skipped, in the preset and in `export okf`, and so is any root-level or domain item merged in from an include (its source file is outside your `.ai-rulez/` directory), because they are not this project's own content; `export okf` names the number skipped on stderr. The bundle is committed documentation: `gitignore = true` never ignores it, and files are written verbatim with no generated-by banner. `generate --check` and `doctor` report a hand-edited, missing or stale bundle file as drift, and `generate` removes concept files whose source is gone. `export okf` keeps a hidden manifest, `.okf-export.json`, at the bundle root listing the files it wrote, and only ever removes files named there: a file you added to the directory, or a bundle exported before the manifest existed, is never pruned (an old export's stale files stay until you delete them; `--check` lists them as `extra`). `--output-dir` is refused, with exit 1 and no change, when it is, contains or lies inside the configuration directory (symlinks resolved), or when it holds a `config.toml`, an `ai-rulez.lock` or a `local/` directory. #### OKF bundles as sources An OKF bundle can be used directly as an include, so a shared knowledge base feeds `generate` without a one-time import: ```toml [[includes]] name = "team-kb" source = "https://github.com/acme/knowledge" # or a local directory ref = "v1.2" # git only; pin a commit for reproducibility path = "bundles/platform" # directory of the bundle inside the source format = "okf" include = ["rules", "skills"] # optional kind filter ``` At load time the bundle is converted with the same mapping as `import okf` (into a temporary directory that is discarded) and merged like any other include, with the usual precedence. The AR001-AR011 security scan runs on the converted text; a bundle with an error-level finding is refused as a whole, and the include is skipped with an error. A bundle in a git repository is cached under `~/.cache/ai-rulez/includes/` and recorded in `ai-rulez.lock` exactly like a git include: `ai-rulez lock` pins a tag or branch to its commit and a content digest, a locked run fetches the pinned commit (a moved tag changes nothing until you relock), `--frozen` and `--offline` use the cache and fail when the digest differs, `lock --check` and `[lock] enforce` apply, and a branch or tag that is not locked is reported as `AR010`. The clone runs with hooks, credential helpers and submodules off and only the https, ssh and file transports. See [Lock file](lockfile.md). `install_to` places the content in a domain like other includes. Configure with `includes[].format`; the only value is `okf`. #### Security - Import never follows symlinks in a bundle: each one is skipped with a warning (AR9B8, listed under `skipped`) and the rest of the bundle is imported. A bundle with paths that differ only in case is refused. Import writes only below the target directory (through `os.Root`, so symlinks in the target cannot redirect a write), and rejects `x-ai-rulez` ids and resource paths that are not plain names (`..`, absolute paths, separators, and resource folders other than `references/`, `scripts/`, `assets/`). - All text to be written, including scripts, is scanned with the `AR001`-`AR011` checks before the first write; one error-level finding (a secret, hidden Unicode, `curl | sh`) refuses the whole import. `ignore` comments inside imported text are not honored. - Bundle size is bounded (50,000 files, 8 MiB per file). Nothing in a bundle is executed. - OKF content is instructions for agents. Importing a rule from a stranger has the same trust implications as any other include: read what `--dry-run` reports. #### CI usage ```bash ai-rulez generate --check # includes the okf preset: fails when docs/okf is stale ai-rulez validate # AR9B0-AR9B9 for the configured bundle ai-rulez okf validate docs/okf --fail-on warning --format json ai-rulez export okf --output-dir /tmp/kb --check # compare without touching the repository ``` Validate a third-party bundle before importing it: ```bash ai-rulez okf validate https://github.com/GoogleCloudPlatform/open-knowledge-format@main#bundles/acme_retail ai-rulez import okf https://github.com/GoogleCloudPlatform/open-knowledge-format@main#bundles/acme_retail --domain acme --dry-run ``` #### Fixtures and licences - `internal/okf/testdata/acme_retail` is a copy of `bundles/acme_retail` from `GoogleCloudPlatform/open-knowledge-format` (commit `ad30107`, Apache-2.0, Copyright Google LLC), without its `viz.html`. It is used read-only as a conformance and import fixture; the notice is in `testdata/NOTICE.txt`. - The other fixtures are synthesized in the tests. No code was copied from okfctl or openknowledge. #### Limitations - Only OKF 0.2 is written. 0.1 bundles (`timestamp`, `# Citations`) are read as they are; nothing is upgraded. - The trust, provenance and lifecycle families (`sources`, `generated`, `verified`, `status`, `stale_after`) and the `Attested Computation` type are preserved as opaque keys but have no meaning in ai-rulez. A failing or stale status is not acted on. - Non-markdown files in a foreign bundle are skipped unless they sit in `references/`, `scripts/` or `assets/` next to a skill. `log.md` is not imported. - A foreign concept maps to exactly one kind; a long `Playbook` becomes a skill named after its path (`runbooks-deploy`). Hand-tune names and descriptions afterwards: skills need a trigger-oriented description. - A title edited in a bundle is kept only through `import okf --force` (see [Titles](#titles-and-conflicts)); a bundle that is exported again without importing loses the edit and is reported as drift. - Roles: `export okf --role r` filters content like `generate --role`, but a knowledge export is not written for a harness, so skills a role serves (`delivery = "served"`) are exported like static ones and a skill's own `delivery` key travels in its `x-ai-rulez.metadata`. `skill_mode` is a Claude Code setting and is not exported. While `generate --role` runs, the `okf` preset leaves the committed bundle untouched instead of shrinking it to the role's slice. - Agents, commands and checks have no OKF type and travel as `Reference` with `x-ai-rulez.kind`. A third-party tool that drops unknown keys loses that information. #### Open questions - The spec is single-vendor and pre-1.0; okf.md describes a different `index.md` and version scheme (see above). Both are supported through `index_style`; the default (`body`) should be revisited once one scheme is clearly adopted. - Whether `Decision`, `Concept` and `Playbook` are the right type names for consumers that route by `type`. The spec leaves it open; they are easy to change in one table. - Whether exporting `agents`, `commands` and `checks` by default is wanted, or only rules, context and skills. ## Domains & Profiles Source: https://goldziher.github.io/ai-rulez/domains/ Organize AI configuration by team, subsystem, or project area. #### What Are Domains? Domains are named areas of your project with their own rules, context, and skills. Use them for: - Multi-team projects (backend, frontend, mobile teams) - Multi-service architectures (API, database, cache, queue) - Feature areas (auth, payments, search) - Environments (dev, staging, production) #### What Are Profiles? Profiles specify which domains are included when generating. Example: ```toml [profiles] full = ["backend", "frontend", "qa"] # All teams backend = ["backend", "qa"] # Backend team only frontend = ["frontend", "qa"] # Frontend team only qa = ["qa"] # QA team only ``` Generation always includes: - All root content (rules, context, skills, agents, commands) - Content from the selected domains only - Globally-active builtins and include-sourced domains, whatever the profile #### Directory Structure ```text .ai-rulez/ ├── config.toml # Main config with presets and profiles ├── rules/ # Base rules (all teams get these) ├── context/ # Base context (all teams get these) ├── skills/ # Base skills (all teams get these) └── domains/ ├── backend/ │ ├── rules/ │ │ ├── api-design.md │ │ └── database.md │ ├── context/ │ │ └── backend-architecture.md │ └── skills/ │ └── database-expert/ │ └── SKILL.md ├── frontend/ │ ├── rules/ │ │ ├── component-guidelines.md │ │ └── accessibility.md │ ├── context/ │ │ └── design-system.md │ └── skills/ │ └── ux-expert/ │ └── SKILL.md └── qa/ └── rules/ └── testing-strategy.md ``` #### Common Organization Patterns ##### Service-Based Domains For microservices or service-oriented architecture: Domains: `api` (REST API service), `database` (database layer), `cache` (caching layer), `queue` (message queue), `frontend` (web UI). **Profiles:** ```toml [profiles] full = ["api", "database", "cache", "queue", "frontend"] backend = ["api", "database", "cache", "queue"] frontend = ["frontend"] infrastructure = ["database", "cache", "queue"] ``` ##### Team-Based Domains For organizations with dedicated teams: Domains: `backend` (Go microservices), `frontend` (React web app), `mobile` (React Native), `qa` (testing and quality assurance), `devops` (infrastructure and deployment). **Profiles:** ```toml [profiles] full = ["backend", "frontend", "mobile", "qa", "devops"] backend-team = ["backend", "qa"] frontend-team = ["frontend", "qa"] mobile-team = ["mobile", "qa"] qa-team = ["qa"] devops-team = ["devops"] ci-all = ["backend", "frontend", "mobile", "qa", "devops"] ``` ##### Feature-Based Domains For projects organized by feature: Domains: `auth` (authentication and authorization), `payments` (payment processing), `notifications` (email, SMS, push), `search` (search and indexing), `analytics` (data collection). **Profiles:** ```toml [profiles] full = ["auth", "payments", "notifications", "search", "analytics"] backend = ["auth", "payments", "notifications", "search", "analytics"] frontend = ["notifications", "search"] ``` ##### Environment-Based Domains For different rules per environment: Domains: `dev` (development guidelines), `staging` (staging constraints), `prod` (production rules), `security-hardened` (extra security measures). **Profiles:** ```toml [profiles] development = ["dev"] staging = ["staging", "security-hardened"] production = ["prod", "security-hardened"] ``` #### Domain Names ##### Good Names Use names that indicate ownership or responsibility: - `backend`, `frontend`, `mobile` (service boundaries) - `api`, `database`, `cache` (technical components) - `auth`, `payments`, `search` (feature areas) - `dev`, `staging`, `prod` (environments) - `golang`, `typescript`, `python` (technology) ##### Avoid - `team1`, `team2` (not descriptive) - `a`, `b`, `c` (unclear) - `everything`, `shared`, `misc` (too broad or ambiguous) #### Creating a Domain ##### Step 1: Create the directory structure ```bash mkdir -p .ai-rulez/domains/backend/{rules,context,skills,agents} mkdir -p .ai-rulez/domains/frontend/{rules,context,skills,agents} ``` ##### Step 2: Add content to the domain **`domains/backend/rules/database.md`:** ```markdown --- priority: critical --- # Database Standards - Use prepared statements to prevent SQL injection - Always add database migrations - Index foreign keys for performance - Document schema changes ``` **`domains/backend/context/architecture.md`:** ```markdown # Backend Architecture ## Services - API Gateway (Go) - User Service (Go) - Product Service (Go) - Order Service (Go) ## Database - PostgreSQL 14+ - Replication enabled - Automated backups ``` ##### Step 3: Update config.toml ```toml version = "5.0" name = "my-platform" presets = ["claude", "cursor"] default = "full" [profiles] full = ["backend", "frontend"] backend = ["backend"] frontend = ["frontend"] ``` ##### Step 4: Generate and test ```bash # Generate for all domains ai-rulez generate --profile full # Generate for backend only ai-rulez generate --profile backend # Generate for frontend only ai-rulez generate --profile frontend ``` #### Using Domains in Your Workflow ##### Single Domain Per Team Backend team only needs backend rules: ```bash # Backend team runs ai-rulez generate --profile backend # Gets: root content + backend content ``` Frontend team only needs frontend rules: ```bash # Frontend team runs ai-rulez generate --profile frontend # Gets: root content + frontend content ``` ##### Multiple Domains Per Person If one person works on multiple areas: ```toml [profiles] full-stack = ["backend", "frontend"] ``` ```bash ai-rulez generate --profile full-stack # Gets: root + backend + frontend content ``` ##### Builtin Packs Per Profile A builtin pack can be scoped to one profile by referencing it as `builtin:` in that profile's domain list: ```toml [profiles] backend = ["backend", "builtin:docker"] frontend = ["frontend"] ``` The `backend` profile gets the `docker` builtin; `frontend` does not. This differs from the root `builtins` field, which makes a pack global once it is enabled. The reference is an explicit opt-in, so it works even when `builtins` is absent or `false`, and the `builtin:` prefix keeps the pack from colliding with a local domain of the same name. A pack the root field already enabled stays global regardless of where else it is named. #### Domain Content Priority When the same name exists in both root and a domain: ```text .ai-rulez/rules/testing.md (root) .ai-rulez/domains/backend/rules/testing.md (domain) ``` The **root version takes precedence** and the domain copy is dropped, with a warning naming both sources. Precedence runs root > on-disk domain > include-sourced domain > builtin, and applies to rules, context, skills, agents and commands alike — see [Deduplication by Name](configuration.md#deduplication-by-name). ```text backend profile gets: - everything from .ai-rulez/rules/*, including the root testing.md - every other backend rule from .ai-rulez/domains/backend/rules/ frontend profile gets: - everything from .ai-rulez/rules/* (including testing.md) - frontend-specific rules from domains/frontend/ ``` To make a rule domain-specific, give it a name no other layer uses, or remove the root copy. #### Composing Profiles Several profiles can be selected at once by separating their names with commas. The result is the union of their domains, de-duplicated, in the order the domains are first named: ```bash ai-rulez generate --profile base,backend ai-rulez tokens --profile base,backend ``` This is what lets one profile hold the content everybody installs while the others add only their own extras: ```toml [profiles] base = ["conventions", "security"] backend = ["api", "database"] frontend = ["web"] ``` `--profile base,backend` and `--profile base,frontend` then cover both roles without a hand-written `base-backend` and `base-frontend` profile each — the combinatorial set a third role would double. A composed value works anywhere a profile name does, including the config's own default and a scope's `profile`: ```toml default = "base,backend" ``` Rules: - **Every element must be a defined profile.** An unknown one is an error naming the element that was wrong, not the whole value. - **Profile values name domains, never other profiles.** Composition is one level deep, so there is no nesting and no cycle to worry about. - **`default` composes only if you defined it.** The built-in `default` profile is a fallback rule rather than a domain list, so it is meaningful only on its own. - **Whitespace and empty elements are ignored**: `base, backend` and `base,backend,` both select the same two profiles. A value that is nothing but separators selects no profile and is reported as not found. - **A profile name may not contain a comma**, since it could then never be selected. #### Roles When the question is "what should a backend engineer or a support agent get on their machine" rather than "what does this repository ship", use [Roles](roles.md): they add per-kind `include` / `exclude` selectors, one level of inheritance and a per-skill `skill_mode` on top of a domain list, and are selected with `generate --role`. #### Advanced Profile Combinations ##### Multi-Level Profiles Profiles can include multiple domains with shared subsets: ```toml [profiles] # Full stack for dev team full-dev = ["backend", "frontend", "devops", "qa"] # Minimal for contractors frontend-only = ["frontend"] # Security-focused for compliance security-audit = ["security", "backend", "database"] # Performance optimization perf-team = ["backend", "database", "cache"] ``` ##### Environment-Specific Profiles ```toml [profiles] # Development: loose constraints dev = ["dev-guidelines", "logging-verbose"] # Staging: stricter staging = ["staging-checks", "logging-standard", "security-checks"] # Production: strictest production = ["production-critical", "logging-minimal", "security-hardened", "compliance"] ``` ##### Feature-Based Selection ```toml [profiles] # New features team features = ["feature-auth", "feature-payments", "feature-notifications"] # Infrastructure team infrastructure = ["database", "cache", "queue", "monitoring"] # Quality team quality = ["testing", "security", "performance", "accessibility"] ``` #### Best Practices ##### Keep Domains Focused Each domain represents one area of responsibility: ```text Good: backend, frontend Bad: backend-with-all-services, frontend-with-all-build-tools ``` ##### Avoid Overlapping Domains If multiple domains need the same rule, put it in root: ```text Root (shared by all): ├── rules/ │ ├── code-quality.md │ ├── security.md │ └── git-workflow.md Domain-specific: ├── domains/backend/rules/ │ └── database.md ├── domains/frontend/rules/ │ └── component-guidelines.md ``` ##### Document Domain Purpose Add comments in config.toml: ```toml # Domains: # - backend: Go services, REST APIs, PostgreSQL # - frontend: React web app, TypeScript # - mobile: React Native iOS/Android # - qa: Testing standards # - devops: Infrastructure, CI/CD, deployment [profiles] full = ["backend", "frontend", "mobile", "qa", "devops"] ``` ##### Use Consistent Names Use the same domain names across projects for clarity. ##### Name Profiles Clearly Profile names should indicate their purpose: ```text Good: full, backend-team, frontend-team Bad: p1, p2 ``` #### Troubleshooting ##### Content Not Appearing Check that your domain is in the profile: ```bash # List available profiles ai-rulez validate --debug # Check config.toml (V4 is TOML: the section is [profiles]) grep -A 5 '^\[profiles\]' .ai-rulez/config.toml ``` ##### Profile Not Found ```bash # Validate configuration ai-rulez validate # Try generating with debug output ai-rulez generate --profile backend --debug ``` ##### Domain Directory Not Recognized Ensure the directory exists and has content: ```bash # Check domain directory ls -la .ai-rulez/domains/backend/ # Domain needs at least one of: rules/, context/, skills/, agents/, commands/ ``` ##### Content Collisions If both root and domain define the same name: ```bash # Lists every collapsed duplicate with its kept and dropped source ai-rulez validate # The root version wins; remove it if you want the domain version instead ``` #### Migration Path If you're starting with a flat structure: ```bash # Current structure .ai-rulez/ ├── rules/ │ ├── backend-api-design.md │ ├── backend-database.md │ ├── frontend-components.md │ └── frontend-styling.md ``` To migrate to domains: 1. Create domain structure: ```bash mkdir -p .ai-rulez/domains/backend/rules mkdir -p .ai-rulez/domains/frontend/rules ``` 2. Move files: ```bash mv .ai-rulez/rules/backend-* .ai-rulez/domains/backend/rules/ mv .ai-rulez/rules/frontend-* .ai-rulez/domains/frontend/rules/ ``` 3. Rename files (remove prefix): ```bash cd .ai-rulez/domains/backend/rules mv backend-api-design.md api-design.md mv backend-database.md database.md ``` 4. Update config.toml: ```toml [profiles] full = ["backend", "frontend"] backend = ["backend"] frontend = ["frontend"] ``` 5. Test: ```bash ai-rulez validate ai-rulez generate --profile full ``` #### Next Steps - **[Quick Start](quick-start.md)**: Getting started with domains - **[Configuration Reference](configuration.md)**: Advanced config options - **[Profiles Guide](profiles.md)**: Creating custom presets ## Roles Source: https://goldziher.github.io/ai-rulez/roles/ A **profile** picks domains for a project. A **role** maps a *job* to the slice of the shared content a person needs: which domains, which skills, rules, agents and commands, and how Claude Code should surface each skill. Profiles answer "what does this repository ship"; roles answer "what does a backend engineer, a data analyst or a support agent get on their machine". !!! note "ai-rulez never does identity" ai-rulez has no notion of users, groups, authentication or the network. A role is a name. Something else (an identity-provider integration, a web UI, a wrapper script) decides which person holds which role and runs `ai-rulez generate --user --role `. The `match` hints on a role are inert strings that tool may read from `roles.json`; ai-rulez stores and republishes them and never interprets them. #### Profiles vs roles Both select a slice of the same content, so it is fair to ask whether profiles are still worth having. They are, because 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`, `[profiles]`, the default profile | `--role` | | Extra knobs | — | `skill_mode`, `delivery`, `match`, `pin` | | Compared by | `tokens --by-profile` | `tokens --by-role` | Reach for a profile when one checkout must emit several *project* variants — a backend service, a frontend app, a QA bundle — and that choice belongs in the repository. Reach for a role when the same content must be tailored to *different people* — a backend engineer, an on-call responder, a support agent — and the choice is made at generation time for that person. A profile is about what the artifact contains; a role is about what one holder sees. Many projects keep a profile for the repository's default and layer roles for the per-person variants. Each axis will meet where it should: **domains** are the shared vocabulary, profiles group domains for a project and roles group domains for a person. A profile also selects [installed skills](installed-skills.md) and MCP servers by their `profiles` field. #### Defining roles Roles are a `[[roles]]` array in `config.toml`. The optional manifest switch lives in a separate `[role_manifest]` table, because TOML cannot use `roles` as both an array and a table. ```toml [role_manifest] enabled = true # write .ai-rulez/roles.json on generate [[roles]] name = "engineer" description = "Everyone who writes code" domains = ["shared", "backend"] # same meaning as a profile's list; "builtin:" works too [roles.skills] exclude = ["deploy-*", "backend/release"] # ids or path.Match globs; "domain/id" matches one domain [roles.skill_mode] "review-*" = "name-only" # on | name-only | user-invocable-only | off deploy = "user-invocable-only" [roles.match] groups = ["okta:eng", "okta:eng-platform"] # free-form hints for an external identity tool [[roles]] name = "release-manager" extends = "engineer" # one level only domains = ["release"] [roles.skill_mode] deploy = "on" # child wins over the parent's "user-invocable-only" ``` | Field | Meaning | | --- | --- | | `name` | Lowercase letters, digits, `-` and `_`. What `--role` takes. Must be unique. | | `description` | Free text shown by `roles list`. | | `domains` | Domains the role selects. Root content, globally active builtins and included domains are always kept, exactly as for a profile. | | `skills`, `rules`, `agents`, `commands`, `checks` | `include` and `exclude` lists. An entry is an item id or a [`path.Match`](https://pkg.go.dev/path#Match) glob; an entry containing `/` is matched against `domain/id`. An empty `include` keeps everything the domains provide; `exclude` always wins. There is no selector for `context`: context files stay. | | `skill_mode` | Skill id or glob to a Claude Code `skillOverrides` state. | | `delivery` | Skill id or glob to `static`, `served` or `both`: how the skill reaches this role's agent (see [Delivery](#delivery)). | | `pin` | `true` records the digest of the role's rendered outputs in `ai-rulez.lock`. Not inherited. | | `extends` | The name of one parent role. | | `match` | `groups`: hint strings for an external tool. Not inherited. | ##### `skill_mode` precedence For one skill, every matching key competes: an exact id beats a glob, a longer glob beats a shorter one, and ties go to the lexically first pattern. The answer never depends on map order. A skill no key matches keeps the default, which is to render it with no override. Claude Code's `skillOverrides` is one flat map keyed by skill id, so two kept skills with the same id in different domains share one entry. When a role resolves them to different modes (for example `backend/deploy = "off"` while `frontend/deploy` has no mode), `validate` reports `AR971`; give both the same mode (a bare `deploy` key does) or exclude one of them. ##### Delivery `[roles.delivery]` decides, per skill, whether the role's agent gets the skill as a file in the harness skill tree (`static`), only on demand from the [skills server](mcp-server.md#dynamic-skill-loading) (`served`), or both. The keys follow the `skill_mode` rules (ids, globs, `domain/id`, the most specific key wins) and the entries are inherited through `extends` with the child winning. ```toml [[roles]] name = "backend" domains = ["backend"] [roles.delivery] "deploy-*" = "served" # not listed in the backend agent's context, found with find_skill "backend/runbooks" = "both" ``` Precedence for one skill: its own `delivery` frontmatter, then the role, then `[domains.] delivery`, then `[skills] delivery`, then `static`. Everything that renders or serves a role uses it: - `generate --role backend` leaves the served skills out of the static trees and adds the `dynamic-skills` stub; - `tokens --role backend` does not count served skills in the listing and names them (`served_skills`); - `ai-rulez mcp --serve-skills --role backend` serves those skills and `find_skill` is scoped to the role; - `roles resolve`, `roles.json` (`items[].delivery`, `totals.served_skills`, `totals.served_tokens`, `delivery`) and `catalog` (`items[].delivery`, `items[].role_delivery`) report it. `ai-rulez lock` pins the served skills of every role, so `[lock] enforce` covers them. ##### Inheritance `extends` is one level deep: a role that extends a role that itself extends another is an error. The child is merged over the parent as follows. | Field | Merge | | --- | --- | | `domains` | union, parent first | | `exclude` lists | union | | `include` lists | union when both roles set one, otherwise whichever is set | | `skill_mode`, `delivery` | merged, the child wins per key | | `description` | the child's, else the parent's | | `match` | **not** inherited: it identifies who holds *this* role | `Validate()` fails hard only on a bad name, a duplicate name, an invalid `skill_mode` value or an invalid `delivery` value. Inheritance problems (unknown parent, cycle, depth greater than one) are logged as warnings there so that `validate` can report them as [AR972](strict-validation.md). A role with broken inheritance is left out of `roles.json` with a warning. Roles are overlayable in `config.local.toml` the same way profiles are: entries merge by `name`, and an entry with `remove = true` drops a shared role. The local overlay schema includes `roles`. !!! warning "A role is a content filter, not an access boundary" A role narrows the rules, skills, agents, commands and checks that are rendered or served, and sets skill modes. It does not narrow hooks, `[permissions]`, `[[mcp_servers]]` or context files: those are rendered for every role. Do not use a role to withhold a tool or a credential from someone; the file-based outputs are plain files the holder can read and edit. #### Commands ```bash ai-rulez roles list [--format json] # every role with item counts and token estimates ai-rulez roles show [--format json] # as declared, and with the parent merged in ai-rulez roles resolve [--format json] # the items the role keeps, with sizes and skill modes # may be a comma-separated composition (see Composing roles below) ai-rulez generate --role engineer # project outputs for the role ai-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 role ai-rulez tokens --by-role # one comparison column per role ai-rulez catalog --format json # items, owners, versions, roles, lock status ``` `--role` and `--profile` are mutually exclusive, and `--role` cannot be combined with `--plugin` (plugin bundles are built from the full content). A role replaces profile selection: the content tree is narrowed by the role's domains and selectors through the same selection path profiles use, so everything downstream in the same run (outputs, the token report, the 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. #### 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, skills, rules, agents and commands they keep are combined, and that union is what is rendered, served and counted. ```bash ai-rulez generate --role engineer,oncall ai-rulez roles resolve engineer,oncall ``` Where two members set `skill_mode` or `delivery` for the same skill, the [within-role precedence](#skill_mode-precedence) applies first — a more specific key (an exact id, then a longer glob) wins — and when two keys are equally specific the role **listed last** wins. Order therefore matters, and a composition is named after its order: `engineer,oncall` and `oncall,engineer` are different, and reversing the list can change a mode. A composition is a value, not a `[[roles]]` entry: it cannot be extended, it need not be declared, and every member must exist (`validate` reports one that does not as `AR971`). A composition is **pinnable**: `ai-rulez lock --role engineer,oncall` records the union's rendered outputs under the canonical name `engineer,oncall`, and `generate --locked --role engineer,oncall` (or `lock --check`) compares them. Pinning one member with `pin = true` does not pin every composition that contains it, and pinning a composition does not pin its members. #### `skill_mode` becomes Claude Code `skillOverrides` For every skill the role keeps and gives a mode, `generate` writes `skillOverrides.` in `.claude/settings.json` (or `~/.claude/settings.json` with `--user`) through the same per-key ownership as [`[claude.settings.managed]`](settings.md): only the listed skill ids are owned, every skill id the role does not list is left alone, `clean` takes back exactly what was recorded, and a second run changes nothing. A skill id the role lists is the role's while the role renders. If you had written a different value for it by hand, `generate --role` warns, replaces it, and remembers yours in `/local/.role-skill-overrides.json` (machine-local, always gitignored); a plain `generate`, or a role that no longer lists the skill, puts your value back. This holds for every run that writes the settings: `generate`, each regeneration of `generate --watch --role`, and `generate --user`; `mcp --serve-skills --role` only serves skills and never writes `.claude/settings.json`, so it changes nothing there. A value you changed after the role wrote it is yours and is left alone. Switching from one role to another removes the first role's entries and writes the second's. A role's modes win over the same skill in `[claude.settings.managed] skill_overrides`. | 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; the user can still run it as `/name` | | `off` | hidden from the model and the user | #### `skill_mode` on other harnesses Only Claude Code has a per-skill settings key. For the other harnesses ai-rulez renders a mode where the vendor documents an equivalent and says so where it does not. One content tree is rendered for every preset of the run, so a mode is written into the skill's `SKILL.md` frontmatter (or, for Codex, `agents/openai.yaml`) and each harness reads what it understands. A key you set in the skill's own frontmatter is never overwritten. | Preset | `user-invocable-only` | `off` | `name-only` | Source (read 2026-10-06) | | --- | --- | --- | --- | --- | | `claude` | `skillOverrides` | `skillOverrides` | `skillOverrides` | [Claude Code skills](https://code.claude.com/docs/en/skills) | | `cursor` | `disable-model-invocation: true` | no documented setting | no | [Cursor skills](https://cursor.com/docs/context/skills) | | `codex` | `agents/openai.yaml` `policy.allow_implicit_invocation: false` | documented (`[[skills.config]] enabled = false`) but needs an absolute path, not rendered | no | [Codex skills](https://developers.openai.com/codex/skills) | | `copilot` | `disable-model-invocation: true` | `disable-model-invocation: true` and `user-invocable: false` | no | [VS Code agent skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills) | | `opencode` | no | documented (`permission.skill` = `deny` in `opencode.json`), not rendered | no | [OpenCode skills](https://opencode.ai/docs/skills/) | | `gemini` | no | only the `/skills disable` command is documented, no settings key | no | [Gemini CLI skills](https://geminicli.com/docs/cli/skills/) | | every other preset | not documented | not documented | not documented | not checked | `on` is every harness's default. A cell that says "no" or "not documented" means ai-rulez writes nothing for it and the skill stays listed on that harness for `user-invocable-only` and `name-only`. For `off`, a skill is hidden only when every configured preset can hide it. Otherwise `generate --role` follows `skill_mode_fallback`: ```toml [role_manifest] skill_mode_fallback = "drop" # drop (default) or serve ``` `drop` leaves the skill out of the role's render; `serve` moves it to [served delivery](#delivery), reachable with `find_skill` on harnesses that can call MCP. The setting lives in `[role_manifest]` because TOML cannot use `roles` as both an array and a table. Because one tree is rendered, the fallback applies to every harness of the run, Claude Code included (its `skillOverrides` entry is then not written for a dropped skill). Each skill and harness that is not honoured is named in a warning. `ai-rulez roles resolve ` lists them (`skill_modes` in `--format json`), and `ai-rulez doctor` reports them. #### Strict validation `generate --role ` prints the AR971 findings of that role as warnings before it writes anything, so a typo in a domain or in an `exclude` entry is not silent. | Code | Meaning | | --- | --- | | [AR971](strict-validation.md) `role-reference-unknown` | A role lists a domain that does not exist, or a selector / `skill_mode` / `delivery` entry that matches no item (or only matches in a domain the role does not select). | | [AR972](strict-validation.md) `role-extends-invalid` | Unknown parent, cycle, or inheritance deeper than one level. | | [AR973](strict-validation.md) `role-unreachable-dependency` | An item the role keeps names a skill in its `skills:` frontmatter that the role drops, or hides from the model with `off` / `user-invocable-only`. Prose references are not analysed. | #### The roles manifest With `[role_manifest] enabled = true`, `generate` writes `/roles.json`. `roles list --format json` prints the same document. It is deterministic (no timestamps; roles sorted by name, items by kind, domain and id), so it is safe to commit, and it is versioned by `schema_version`. It is built from the shared sources only: roles declared in `config.local.toml` and items under `local/` never appear in it, so the committed file is the same on every machine and `generate --check` does not report it as drifted. Local roles still work with `generate --role` and `roles list`. The JSON schema is [`schema/roles-manifest.schema.json`](https://github.com/Goldziher/ai-rulez/blob/main/schema/roles-manifest.schema.json). ```json { "schema_version": 1, "tokenizer": "cl100k_base", "roles": [ { "name": "engineer", "description": "Everyone who writes code", "match": { "groups": ["okta:eng"] }, "domains": ["shared", "backend"], "skill_modes": { "review-pr": "name-only" }, "delivery": { "deploy-*": "served" }, "items": [ { "kind": "skill", "id": "review-pr", "domain": "shared", "path": "domains/shared/skills/review-pr/SKILL.md", "mode": "name-only", "delivery": "static", "owner": "platform", "version": "1.2.0", "bytes": 2210, "tokens": 540 } ], "totals": { "items": 1, "bytes": 2210, "tokens": 540, "by_kind": { "skill": 1 }, "served_skills": 0, "served_tokens": 0 } } ] } ``` `bytes` is the size of the item's source files on disk (a skill counts its resources). `tokens` is an estimate for the primary file (`SKILL.md`, the rule file, ...) and is an approximation, like every figure of `ai-rulez tokens`. `delivery` (skills only) is how the skill reaches the role's agent; `served_skills` and `served_tokens` total the skills that are served only, which cost no listing tokens until `load_skill` runs. `owner` and `version` come from the item's frontmatter when present. #### Integrating an identity tool or UI Everything a tool needs is reachable from `--format json` output and versioned documents. ai-rulez never calls the network, and an integration never needs to parse config files. 1. **Discover the roles.** Read `roles.json` (committed) or run `ai-rulez roles list --format json`. Each role has its `name`, its `description`, and `match.groups`, the hints you put there for your tool. 2. **Map a person to a role.** This is your tool's job. For example, an `acli` command can read a person's identity-provider groups, find the role whose `match.groups` contains one of them (choosing by your own precedence when several match), and print the role name. ai-rulez does not look at `match`. 3. **Preview.** `ai-rulez roles resolve --format json` lists exactly the items the role keeps, with owner, version, bytes and tokens. `ai-rulez tokens --role --format json` reports the token surface. 4. **Apply.** Run `ai-rulez generate --user --role --yes` (user level) or `ai-rulez generate --role ` (project level). Exit code 0 means the files were written. 5. **Show the whole catalog.** `ai-rulez catalog --format json` lists every item with its owner, version, size, sha256 digest, the roles that keep it, a summary of each role and the lock status. Its schema is [`schema/catalog.v1.schema.json`](https://github.com/Goldziher/ai-rulez/blob/main/schema/catalog.v1.schema.json) (`--schema-version 2` adds load cost, lint and excerpts; see [Catalog](catalog.md)). 6. **Audit.** [`ai-rulez lock --diff --format json`](lockfile.md) reports what changed between the committed lock and the working tree ([`schema/lock-diff.schema.json`](https://github.com/Goldziher/ai-rulez/blob/main/schema/lock-diff.schema.json)). Every JSON document carries `schema_version`; a consumer should refuse a version it does not know. A UI needs no other interface: lists come from `roles.json` / `catalog`, previews from `roles resolve`, the action is one `generate` command. #### Roles and the lock file The [lock file](lockfile.md) pins each role declaration as an item (`kind = "role"`), so adding a role, or changing what a role includes, shows up in the lock diff and is caught by `lock --check`. The lock pins the default rendering; 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 ]` and `generate --check --locked --role ` then catch a change in what the role renders that leaves the sources alone, for example a new `skill_mode` entry or a generator release; see [Composing with roles](lockfile.md#composing-with-roles). Roles without a pin are unchanged, and `generate --locked --role ` still verifies that the *sources* match the lock. Skills a role delivers as `served` are pinned as `[[served]]` entries (see [Served skills and skill sources](lockfile.md#served-skills-and-skill-sources)), so `[lock] enforce` holds for a server started with `--role`. Checks are a role-selectable kind and are pinned like rules. ## Includes Source: https://goldziher.github.io/ai-rulez/includes/ Reuse configurations across multiple projects through inheritance and composition. #### Overview Includes work through configuration inheritance: 1. Define common rules once in a shared configuration 2. Share rules, context, skills, agents, and commands across projects 3. Mix and match includes to create project-specific configurations 4. Track changes through version control #### How Includes Work Includes allow one `.ai-rulez/` configuration to inherit content from other configurations. Use for: - Organization-wide coding standards - Framework-specific guidelines (React, Go, Python) - Consistent security policies - Team-specific workflows #### Basic Example ##### Creating a Shared Configuration Create a `.ai-rulez/` directory that others can include: **`shared-rules/.ai-rulez/config.toml`:** ```toml version = "5.0" name = "shared-rules" description = "Organization-wide AI rules" presets = [] [profiles] default = [] ``` **`shared-rules/.ai-rulez/rules/security.md`:** ```markdown --- priority: critical --- # Security Standards - Always validate user input - Use parameterized queries - Never hardcode secrets - Rotate credentials regularly ``` ##### Including in Your Project In your project's `.ai-rulez/config.toml`, reference the shared rules: ```toml version = "5.0" name = "my-project" # Include rules from another directory includes = [ { name = "shared-rules", source = "../shared-rules/.ai-rulez" } ] presets = ["claude", "cursor"] [profiles] default = [] ``` Now your project includes all content from the shared configuration. #### Include Paths Includes can be: 1. Relative paths: `../shared-rules/.ai-rulez`, `./team-guidelines/.ai-rulez` 2. Absolute paths: `/etc/ai-rulez-standards/.ai-rulez` 3. Git URLs: `https://github.com/org/shared-rules.git`, `git@github.com:org/shared-rules.git` !!! warning "Local paths must stay inside the project" A local path (or `local_override`) in the project's committed config must resolve inside the project, after symlinks are resolved: a repository could otherwise name `../victim` or `~/.config` and have that content written into its generated outputs. A path outside the project stops loading with an error naming it (it is not skipped with a warning). Set it in the machine-local overlay (`config.local.toml`) or declare the include in your user config (`generate --user`). The examples below that use `../` or an absolute path need one of these. A `file://` git URL is a local path too and follows the same rule, for includes and `skill_sources`: it must name a repository inside the project, unless it comes from the machine-local overlay or the user config. A remote git URL (`https://`, `ssh://`, `git@host:path`) is not affected. To let a project config use `file://` repositories elsewhere on your machine, set `AI_RULEZ_ALLOW_FILE_URLS=1` in your own environment. ##### Examples **Sibling directory:** ```toml [[includes]] name = "shared-rules" source = "../shared-rules" include = ["rules", "context"] merge_strategy = "local-override" ``` **Subdirectory:** ```toml [[includes]] name = "team-config" source = "./config/shared" include = ["rules", "skills"] merge_strategy = "local-override" ``` **Git repository (HTTPS):** ```toml [[includes]] name = "org-standards" source = "https://github.com/myorg/shared-rules.git" ref = "main" include = ["rules", "context", "skills", "agents"] merge_strategy = "local-override" ``` **Git repository (SSH):** ```toml [[includes]] name = "company-policies" source = "git@github.com:company/ai-rulez.git" ref = "v1.2.3" include = ["rules", "context"] merge_strategy = "local-override" ``` **Multiple includes:** ```toml [[includes]] name = "team-guidelines" source = "../team-guidelines" include = ["rules", "context"] merge_strategy = "local-override" [[includes]] name = "org-standards" source = "git@gitlab.com:org/standards.git" ref = "main" include = ["rules", "skills"] merge_strategy = "local-override" [[includes]] name = "security-policies" source = "./security-policies" include = ["rules"] merge_strategy = "local-override" ``` ##### Supported Git URL Formats - **HTTPS:** `https://github.com/owner/repo.git` - **SSH:** `git@github.com:owner/repo.git` - **SSH protocol:** `ssh://git@github.com/owner/repo.git` - **GitLab:** `https://gitlab.com/owner/repo.git`, `git@gitlab.com:owner/repo.git` - **`git+` prefix:** accepted, as for skill sources and `--source`: `git+https://host/org/repo` is the same include as `https://host/org/repo` and shares its cache. The lock records the source as you wrote it. - **Self-hosted GitLab:** `git@git.example.com:owner/repo.git`, `https://git.example.com/owner/repo.git` ##### SSH Cloning for Private Repositories For private repositories that require SSH authentication, ai-rulez automatically uses `git clone` when it detects SSH URLs (`git@...` or `ssh://...`). This leverages your existing SSH key configuration. **Benefits of SSH cloning:** - Works with private repositories without needing access tokens - Uses your configured SSH keys and agent - Supports self-hosted Git servers (GitLab, Gitea, Gogs, etc.) - Ideal for local development and multi-repo setups **Example with SSH:** ```toml [[includes]] name = "private-rules" source = "git@git.example.com:company/ai-rulez.git" ref = "main" include = ["rules", "context"] merge_strategy = "local-override" ``` **Requirements:** - Git must be installed and available in your PATH - SSH keys must be configured for the git host - SSH agent should be running (for passphrase-protected keys) ##### Repository Structure Support An include source does **not** have to wrap its content in a `.ai-rulez/` folder. ai-rulez supports both a wrapped and a bare (flattened) layout and auto-detects which one a source uses. 1. **Wrapped structure**: Content lives inside a `.ai-rulez/` subdirectory ```text my-repo/ ├── .ai-rulez/ │ ├── config.toml │ ├── rules/ │ ├── context/ │ ├── skills/ │ └── agents/ └── other files... ``` 2. **Bare / flattened structure** (recommended for shared-module repos): the directory exposes `rules/`, `context/`, `skills/`, and `agents/` directly — no `.ai-rulez/` wrapper needed. This works at the repository root **or** at any sub-path: ```text my-repo/ └── modules/ └── core/ ├── rules/ ├── context/ ├── skills/ └── agents/ ``` Point an include at the sub-path with the `path` field: ```toml [[includes]] name = "core" source = "https://github.com/org/shared-modules.git" path = "modules/core" # resolves modules/core/rules, modules/core/skills, ... include = ["rules", "skills"] merge_strategy = "local-override" ``` ai-rulez detects the layout automatically: it first looks for a `.ai-rulez/` directory (at the source root or under `path`), and otherwise treats a directory that contains any of `rules/`, `context/`, `skills/`, `agents/`, or `commands/` as a bare ai-rulez structure. The flat layout keeps shared modules — especially skill-first modules that ship mostly `skills//SKILL.md` — clean and free of boilerplate wrapping. !!! note `ai-rulez include add` with a **local** path validates that the directory contains a `.ai-rulez/` subdirectory, so a bare/flattened *local* source is rejected by the CLI even though the resolver accepts it. Add a bare local include by editing `config.toml` directly (git sources are not restricted this way). ##### Private Repository Authentication (HTTPS) When working with private Git repositories in includes, you can authenticate using an access token. ###### Using Environment Variable (Recommended) Set the `AI_RULEZ_GIT_TOKEN` environment variable with your access token: ```bash export AI_RULEZ_GIT_TOKEN="ghp_your_github_token_here" ai-rulez generate ``` This is the recommended approach for CI/CD environments and automation scripts. ###### Which hosts receive the token The token is sent only to `github.com` by default. To use it with another host, list the hosts in the environment (a project file cannot widen this): ```bash export AI_RULEZ_GIT_TOKEN_HOSTS="github.com,gitlab.example.com" ``` The list replaces the default. An include that names any other HTTPS host is fetched without the token and logs a warning. The token is passed to git as a header scoped to the host, not embedded in the URL, so it does not appear in process arguments or in the cached clone's `.git/config`. Plain `http://` and `git://` remotes are rejected. ###### Using CLI Flag Pass the token directly via the `--token` flag: ```bash ai-rulez generate --token "ghp_your_github_token_here" ``` ###### Creating Access Tokens **GitHub:** 1. Go to Settings → Developer settings → Personal access tokens 2. Click "Generate new token (classic)" 3. Select scopes: - `repo` - Required for accessing private repositories 4. Generate and copy the token **GitLab:** 1. Go to User Settings → Access Tokens 2. Click "Add new token" 3. Select scopes: - `read_repository` - Required for reading private repositories 4. Create token and copy it **Other Git Hosts:** Most Git hosting platforms that support Bearer token authentication will work with ai-rulez. The token is sent as a Bearer token in the Authorization header when fetching repository archives. ###### Security Best Practices - **Never commit tokens** to your repository or configuration files - **Keep credentials out of the URL**: `ai-rulez include add` and `skill install` refuse a source like `https://user:token@host/org/repo.git`, because it would be written in clear into `config.toml`. Use `AI_RULEZ_GIT_TOKEN` or the git credential helper instead; `validate`/`scan` report an already committed credentialed source as `AR035` (see [Strict validation](strict-validation.md)) - **Use environment variables** in CI/CD pipelines (GitHub Actions secrets, GitLab CI/CD variables, etc.) - **Store tokens securely** using secret management systems (AWS Secrets Manager, HashiCorp Vault, etc.) - **Rotate tokens regularly** to limit exposure from potential leaks - **Use minimal permissions** - only grant the token access to what's needed (read-only repository access) - **Use organization-level tokens** when possible to manage access centrally ###### Example: CI/CD Integration **GitHub Actions:** ```yaml name: Generate AI Rules on: [push] jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Generate env: AI_RULEZ_GIT_TOKEN: ${{ secrets.GIT_TOKEN }} run: npx ai-rulez@latest generate ``` **GitLab CI:** ```yaml generate: script: - export AI_RULEZ_GIT_TOKEN="$CI_JOB_TOKEN" - ai-rulez generate ``` ##### Include Options - **`name`**: Unique identifier for the include - **`source`**: Path or Git URL to the configuration - **`path`**: (Optional) Sub-path within the source to resolve (e.g. `modules/core`). Supports the bare/flattened layout described above; defaults to the source root - **`ref`**: (Git only) Branch, tag, or commit SHA. Defaults to the remote's default branch (`HEAD`), not necessarily `main`. - **`version`**: (Git only) A semantic-version range such as `^1.2` or `~2.1.0`, resolved against the repository's tags and pinned in the lock; instead of `ref` (not both). `tag_prefix` and `include_prerelease` refine it, and `min_release_age = "7d"` holds back tags younger than that. Only `ai-rulez update` moves the pin. See [Version constraints](lockfile.md#version-constraints). - **`include`**: List of content types to fetch: `rules`, `context`, `skills`, `agents`, `commands`. MCP servers are not importable from an include. - **`install_to`**: (Optional) Import the included content into a specific domain instead of the root. - **`local_override`**: (Optional) A local path used **instead of** `source` (for example a checkout you are developing). It is resolved against the project directory, and `path` is appended to it. If the directory does not exist, the include is skipped silently (an info line is logged); the remote source is not used as a fallback. `local_override` bypasses the lock, so it is honoured only when set in the machine-local overlay (`config.local.toml`) or when no lock is enforced: `generate --locked`, `--frozen` and an enforced lock fail on a `local_override` in the committed config. - **`merge_strategy`**: How to handle conflicts: - `local-override`: local content takes precedence (default) - `include-override`: the included content takes precedence - `error`: fail generation on a conflict Each include carries its own strategy. Includes are not recursive: an include contributes its own content, and any `includes` declared in the included configuration are ignored. Domains from an include are always carried over; `include` filters content kinds, not domains. An include that cannot be created, fetched or merged is an error: the command exits `1` and names the include. A remote include that is unreachable falls back to its cached copy when one exists; with no cache there is nothing to render. This holds for `generate`, `generate --check`, `validate`, `doctor` and every other command that loads the configuration, so a CI gate cannot pass on a checkout that renders without the include. Under `--offline` (cached content only) an include that cannot be resolved is logged as a warning and skipped instead, and `ai-rulez lock` reports it as a problem of its own. An `[[installed_skills]]` entry follows the same rule: a skill that cannot be fetched, found (no `SKILL.md` at its path) or scanned fails the load instead of being dropped, which would have removed its generated outputs as stale. `--offline`, `lock`, `sbom` and the CRUD commands keep the warning. A `local_override` that names a missing directory skips the skill with a notice, as it does an include. Included content never follows symlinks: a symlinked file or directory (a rule, skill, agent, command, context or check file, `rules/`, `skills/` and so on, domains, or the include's `.ai-rulez/` itself) is skipped with a warning naming it, so an include cannot point at an arbitrary local file. Replace the link with the real file or directory. An installed skill whose `SKILL.md` is a symlink fails to resolve, with an error naming the link, and a symlinked path inside its clone is skipped with a warning naming it. (Your own project's `.ai-rulez/` may use symlinks that stay inside the project; see [Configuration](configuration.md#symlinks-in-content).) Include URLs are classified as git for `http(s)://`, `file://`, `ssh://`, `git://` and `user@host:path` (`http://` and `git://` are then rejected); anything else is a local path. Remote includes are cached under `~/.cache/ai-rulez/includes/-` with mode `0700`. ##### Machine-local includes and offline runs - `ai-rulez include add --local` (and the MCP `add_include` with `local: true`) writes the include to your gitignored `config.local.*` overlay instead of the shared config, so a personal checkout can be included without affecting teammates. Overlay includes merge by name with the shared list; `remove = true` hides a shared include on your machine. See [Local Configuration](local-overrides.md). - `ai-rulez generate --offline` skips network fetches and uses cached content for remote includes. CRUD commands that validate a change (including the `--local` ones) read remote includes from cache only. ##### Pinning remote includes A git include without a `ref` follows the repository's default branch, so two machines can generate different output from the same config. `ai-rulez lock` records each remote include's resolved commit and a content digest in `.ai-rulez/ai-rulez.lock`; later `generate` runs fetch that commit and verify the digest. `generate --locked` (CI) fails on a source the lock does not cover, and `--frozen` never uses the network. `ai-rulez.lock` also pins your own authored content (`sha256` per rule, skill, hook and role, plus the generated outputs); see the [Lock file](lockfile.md) and the [Lock Command](cli.md#lock-command). #### Include Priority Resolution starts from your local content and folds each include in declaration order, using that include's strategy (default `local-override`, which means *the existing base wins*): 1. Your local configuration has the highest priority. 2. The first include's content becomes part of the base and therefore beats later includes of the same name. 3. A later include only wins where its `merge_strategy` is `include-override`. ```toml [[includes]] name = "base" source = "../base-rules/.ai-rulez" # loaded first [[includes]] name = "team" source = "../team-rules/.ai-rulez" # only overrides "base" with merge_strategy = "include-override" merge_strategy = "include-override" ``` If `base` and `team` both define `rules/security.md`, `base` wins unless `team` sets `merge_strategy = "include-override"`. Your own `rules/` directory always wins. #### Common Patterns ##### Organization-Wide Standards Create a central repository with baseline rules: **Repository structure:** ```text org-standards/ └── .ai-rulez/ ├── config.toml ├── rules/ │ ├── security.md │ ├── code-quality.md │ └── git-workflow.md └── context/ └── company-values.md ``` **Each project includes it:** ```toml [[includes]] name = "standards" source = "https://github.com/myorg/standards.git" path = ".ai-rulez" ``` ##### Framework-Specific Rules Create separate includes for each framework: ```text frameworks/ ├── go-backend/ │ └── .ai-rulez/ │ ├── config.toml │ └── rules/ │ ├── project-layout.md │ ├── error-handling.md │ └── testing.md ├── react-frontend/ │ └── .ai-rulez/ │ └── rules/ │ ├── component-guidelines.md │ ├── hooks-patterns.md │ └── styling.md └── python-ml/ └── .ai-rulez/ └── rules/ ├── numpy-conventions.md └── ml-best-practices.md ``` **Your project uses them:** ```toml presets = ["claude", "cursor"] [[includes]] name = "go-backend" source = "../../frameworks/go-backend/.ai-rulez" [[includes]] name = "react-frontend" source = "../../frameworks/react-frontend/.ai-rulez" [profiles] backend = ["backend"] frontend = ["frontend"] full = ["backend", "frontend"] ``` ##### Monorepo with Shared and Team-Specific Rules **Repository structure:** ```text monorepo/ ├── shared-rules/.ai-rulez/ # Used by all teams ├── backend-team/ │ └── .ai-rulez/ # Includes shared + backend-specific ├── frontend-team/ │ └── .ai-rulez/ # Includes shared + frontend-specific └── mobile-team/ └── .ai-rulez/ # Includes shared + mobile-specific ``` **`backend-team/.ai-rulez/config.toml`:** ```toml version = "5.0" name = "backend-api" includes = [ { name = "shared-rules", source = "../shared-rules/.ai-rulez" } ] presets = ["claude", "cursor"] [profiles] default = [] ``` #### Flat Composition Includes do not nest. To combine several layers, list each one in the project's own `includes`: ```text org-base/.ai-rulez/ rules/security.md go-framework/.ai-rulez/ rules/testing.md team-standards/.ai-rulez/ rules/review.md my-project/.ai-rulez/ config.toml with one [[includes]] entry per directory above ``` An `includes` list inside `go-framework`'s own `config.toml` is ignored when `my-project` includes it, so `org-base` must be listed in `my-project` too. Includes fold in declaration order (see [Include Priority](#include-priority)). #### Collision Handling When includes define the same file: ```text shared-rules/.ai-rulez/rules/testing.md go-framework/.ai-rulez/rules/testing.md my-project/.ai-rulez/rules/testing.md ``` Under the default `local-override` strategy the earliest source wins, and your own content beats every include: 1. `my-project/rules/testing.md` (your project content, highest priority) 2. `shared-rules/rules/testing.md` (first include becomes part of the base) 3. `go-framework/rules/testing.md` (only wins if it sets `merge_strategy = "include-override"`) The losing copy is dropped; `generate` and `validate` log a `Duplicate rule collapsed` warning naming the kept and dropped paths (see [Deduplication by Name](configuration.md#deduplication-by-name)). With `merge_strategy = "error"`, the conflict fails generation instead. #### Best Practices ##### Organize by Specificity ```text org-wide-rules/ Applies to everything team-rules/ Team-specific framework-rules/ Technology-specific project-rules/ Project-specific ``` ##### Use Clear Naming Good: - `org-standards/.ai-rulez` - `react-best-practices/.ai-rulez` - `backend-security/.ai-rulez` Bad: - `rules/.ai-rulez` (ambiguous) - `base/.ai-rulez` (unclear scope) ##### Document Includes Add comments to your config explaining why includes are needed: ```toml version = "5.0" name = "my-backend" # Organization-wide coding standards # Go-specific conventions # Backend team standards includes = [ { name = "org-standards", source = "../org-standards/.ai-rulez" }, { name = "go-guidelines", source = "../go-guidelines/.ai-rulez" }, { name = "backend-team", source = "../backend-team/.ai-rulez" } ] presets = ["claude", "cursor"] ``` ##### Keep Includes Focused Each include should have a single purpose: ```text Good: security-policies/ Rules for security error-handling/ Rules for error handling code-style/ Rules for code style Bad: everything/ Security, errors, style, testing, etc. ``` ##### Version Your Includes Tag releases and reference specific versions: ```bash git tag v1.0.0 org-standards/ ``` ```toml [[includes]] name = "standards" source = "https://github.com/org/standards/.ai-rulez" ref = "v1.0.0" ``` #### Troubleshooting ##### Include Path Not Found ```bash ls -la ../shared-rules/.ai-rulez/config.toml ``` ```toml # Or point at an absolute path [[includes]] name = "shared" source = "/path/to/shared-rules/.ai-rulez" ``` ##### Include Skipped or Nested Includes Ignored Includes are not recursive, so cycles cannot occur. Give each include a unique name: duplicate names are not rejected and are processed independently. If an include's content is missing from the output, run `ai-rulez validate --debug`: a failed include is logged as `Failed to process include` and skipped (an error under `--locked`, `--frozen` or an enforced lock), and a `local_override` path that does not exist skips the include silently. ##### Conflicting Rules Use project-level rules to override, or change which include wins with `merge_strategy`: ```toml [[includes]] name = "stricter" source = "../stricter-rules/.ai-rulez" # loads first, wins by default [[includes]] name = "lenient" source = "../lenient-rules/.ai-rulez" merge_strategy = "include-override" # makes this one win instead ``` Your `.ai-rulez/rules/security.md` overrides every include. ##### Content Not Merging ```bash cat .ai-rulez/config.toml | grep includes ls -la ../shared-rules/.ai-rulez/ ai-rulez validate --debug ``` #### Migration Path If you're currently using separate configurations: 1. **Extract common rules** into a shared include: ```bash mkdir -p ../shared-rules/.ai-rulez/rules # Move common rules there ``` 2. **Add include** to your config: ```toml [[includes]] name = "shared" source = "../shared-rules/.ai-rulez" ``` 3. **Regenerate** and test: ```bash ai-rulez validate ai-rulez generate ``` 4. **Commit** the include: ```bash git add ../shared-rules/ git commit -m "chore: extract shared rules" ``` #### Next Steps - **[Configuration Reference](configuration.md)**: Advanced config options - **[Domains & Profiles](domains.md)**: Team organization - **[Quick Start](quick-start.md)**: Getting started ## Installed Skills Source: https://goldziher.github.io/ai-rulez/installed-skills/ Install named skills from external repositories. Skills are fetched dynamically at generate time and included in your outputs — no local copy needed. #### Overview Installed skills let you pull specialized AI instructions from any git repository or local path. Unlike [includes](includes.md) (which import `.ai-rulez/` content), installed skills expect a simpler structure: a `skills//SKILL.md` file at the repository root. Use installed skills to: - Add library-specific instructions (e.g., how to use a framework or SDK) - Share team skills across projects without full includes - Distribute AI guidance alongside your library #### Installed skills vs skill sources ai-rulez can reach a skill in another repository two ways, and they differ in *what lands where*. | | Installed skill (`[[installed_skills]]`) | Skill source (`[[skill_sources]]`) | | --- | --- | --- | | What it is | One named skill fetched at generate time | A repository whose skills are served on demand | | Where it goes | Written into the harness skill trees, like a local skill | Never written; served over MCP and found with `find_skill` | | Reaches the project as | A content file, so profiles and roles scope it | A served skill, so the [skills server](mcp-server.md#dynamic-skill-loading) and a role's `delivery` scope it | | Use it for | A skill the agent should always have in context | A catalog of skills the agent should find on demand | Both are pinned in `ai-rulez.lock` and scanned before they are used. Reach for an **installed skill** when the skill belongs in the agent's static context (a library's usage guide), and for a **skill source** when you want many skills available without paying their tokens up front (a vendor's whole skill catalog). Both select several skills from one repository (`include`/`exclude`), namespace their names (`name_prefix`) and set the scan level of what they import (`trust`). `ai-rulez skill install`, `list`, `update` and `remove` manage installed skills; skill sources are declared in `[[skill_sources]]` in `config.toml`. #### Quick Start ```bash # Install a skill ai-rulez skill install kreuzberg --source https://github.com/kreuzberg-dev/kreuzberg # List installed skills ai-rulez skill list # Generate — the skill is fetched and included automatically ai-rulez generate # Remove when no longer needed ai-rulez skill remove kreuzberg ``` #### Configuration Installed skills are defined in `.ai-rulez/config.toml`: ```toml [[installed_skills]] name = "kreuzberg" source = "https://github.com/kreuzberg-dev/kreuzberg" [[installed_skills]] name = "ai-rulez" source = "https://github.com/Goldziher/ai-rulez" ref = "main" [[installed_skills]] name = "custom-skill" source = "https://github.com/org/repo" path = "custom/skill/path" # defaults to skills/ # One entry can install several skills from one repository. [[installed_skills]] name = "vendor" source = "https://github.com/org/skills" path = "skills" # a directory of skill directories include = ["db-*", "api"] # keep only these directory names exclude = ["*-wip"] # drop these; exclude wins over include name_prefix = "vendor-" # every resolved skill is named vendor- trust = "warn" # only error-severity scan findings block generate ``` ##### Fields | Field | Required | Description | | ---------------- | -------- | ---------------------------------------------------------------- | | `name` | Yes | Unique skill name | | `source` | Yes | Git URL or local path to the repository | | `path` | No | Path within repo to the skill directory, or to a directory of skill directories. Defaults to `skills/` | | `ref` | No | Git ref (branch, tag, or commit). Defaults to the repository's default branch (`HEAD`) | | `local_override` | No | Local path override for development | | `profiles` | No | Restrict the skill to the named profiles. Omit to include it in every profile | | `domain` | No | Tie the skill to a domain so a profile or a role selects it by domain, exactly as it selects local content. Omit for global | | `include` | No | Keep only skills whose directory name matches one of these globs. Empty keeps every skill below `path` | | `exclude` | No | Drop skills whose directory name matches one of these globs; wins over `include` | | `name_prefix` | No | Prepend this to every resolved skill name, so two entries cannot collide | | `trust` | No | Scan level of the imported skills: `error` (every finding counts, the default) or `warn` (only error-severity findings block) | One entry resolves to one skill when the directory `path` names holds a `SKILL.md`, and to the `SKILL.md`-bearing subdirectories of `path` otherwise, selected by `include`/`exclude` and named with `name_prefix`. A single-skill entry behaves exactly as it did before these fields existed. #### CLI Commands ##### `ai-rulez skill install --source [flags]` Install a named skill. **Flags:** - `--source ` (required): Git URL or local path - `--path ` (optional): Path within repo to skill directory - `--ref ` (optional): Git ref (branch, tag, commit) - `--local` (optional): Record the skill in your gitignored `config.local.*` overlay instead of `config.toml`, so it applies on this machine only. See [Local Configuration](local-overrides.md) `skill install` refuses a `--source` URL that carries a credential in its userinfo (`https://user:token@host/...`): writing it would commit the secret to `config.toml` in clear. Keep the URL free of secrets and authenticate with the git credential helper or the `AI_RULEZ_GIT_TOKEN` environment variable; `validate`/`scan` report an already committed credentialed source as `AR035` (see [Strict validation](strict-validation.md)). **Examples:** ```bash # From a git repo (skill at skills/kreuzberg/) ai-rulez skill install kreuzberg --source https://github.com/kreuzberg-dev/kreuzberg # With explicit path and ref ai-rulez skill install my-lib --source https://github.com/org/repo --path libs/my-lib --ref v2.0 # From a local path ai-rulez skill install local-skill --source ../my-other-repo ``` ##### `ai-rulez skill remove [flags]` Remove an installed skill from the configuration. **Flags:** - `--yes`, `-y` (optional): Skip confirmation prompt - `--local` (optional): Remove through the overlay. A skill defined in the shared config is hidden on this machine by writing `remove = true` to the overlay; the shared config is not changed ##### `ai-rulez skill list [flags]` List all installed skills. **Flags:** - `--format text|json` (optional, default `text`): `json` prints JSON #### How It Works 1. During `ai-rulez generate`, each installed skill is fetched from its source 2. The skill directory (`SKILL.md` + optional `references/`) is read 3. Reference files are concatenated into the skill content 4. The skill is added to the root-level content tree 5. If a local skill has the same name, the local skill wins (installed skill is skipped with a warning) #### Skill Directory Structure Installed skills expect this layout in the source repository: ```text repo-root/ skills/ my-skill/ SKILL.md # Required: main skill content references/ # Optional: additional reference docs api-reference.md configuration.md ``` ##### SKILL.md Format ```yaml --- name: my-library description: >- Brief description of what this skill covers. license: MIT metadata: author: your-org version: "1.0" repository: https://github.com/your-org/your-repo --- # My Library Instructions for AI assistants working with your library... ``` A missing `description` produces a warning rather than an error; the skill name is used as a fallback. Set it anyway — the description is how the assistant decides when to load the skill. ##### References Files in `references/` (like `scripts/` and `assets/`) are kept as separate files next to the generated `SKILL.md`, for both path and git sources; they are not appended to the skill body. This lets you separate detailed API docs, configuration guides, etc. from the main skill instructions, and the assistant loads them on demand. #### Pinning with a lock file `ref` defaults to the repository's default branch (`HEAD`), which moves. Run `ai-rulez lock` (or `ai-rulez skill update `) to record the resolved commit and a content digest in `.ai-rulez/ai-rulez.lock`. Later `generate` runs fetch exactly that commit and fail if the files do not match the digest; `generate --locked` fails when a skill is not covered, and `--frozen` never uses the network. See the [Lock Command](cli.md#lock-command). Imported skill text is instruction text: with `[lint.security] scan_imports`, it is also scanned for secrets, hidden characters and risky commands before it is written ([Security checks](strict-validation.md#security-checks)). To serve a repository of skills over MCP instead of writing them into the skill trees, use `[[skill_sources]]` or `ai-rulez mcp --serve-skills --source`; they are pinned in the same lock (kind `source`). See [Dynamic skill loading](mcp-server.md#skill-sources). #### Local Override For development workflows, use `local_override` to point to a local checkout instead of fetching from git: ```toml [[installed_skills]] name = "my-lib" source = "https://github.com/org/my-lib" local_override = "../my-lib" ``` If the local path exists and contains the skill, it's used. If it doesn't exist, the skill is skipped (not fetched from git). #### MCP Tools The MCP server exposes three tools for managing installed skills: - `install_skill` — Install a skill (params: `name`, `source`, `path`, `ref`, `local`) - `uninstall_skill` — Remove a skill (params: `name`, `local`) - `list_installed_skills` — List all installed skills (shared layer only; takes no `local`) `local: true` writes to the `config.local.*` overlay instead of the shared config. #### Creating Distributable Skills To make your project's skill installable by others: 1. Create `skills//SKILL.md` at your repository root 2. Add YAML frontmatter with `name`, `description`, and optional `metadata` 3. Write comprehensive instructions in the skill body 4. Optionally add `references/*.md` for detailed reference documentation 5. Users install with: `ai-rulez skill install --source ` #### Comparison with Includes | Feature | Includes | Installed Skills | | ---------------- | ------------------------------------------------------ | ----------------------------------------- | | Content types | Rules, context, skills, agents, commands | Skills only | | Source structure | A `.ai-rulez/` directory or a bare layout (`rules/`, `skills/`, ...), see [Repository Structure Support](includes.md#repository-structure-support) | Requires `skills//SKILL.md` | | Merge strategy | Configurable (local-override, include-override, error) | Local skills always win | | Domain support | Can install to specific domains | Root-level only | | Use case | Share full governance configs | Add library/tool-specific AI instructions | ## Local Configuration Source: https://goldziher.github.io/ai-rulez/local-overrides/ Personal, machine-local configuration that is never committed. Use it for scratch notes, per-machine paths, personal presets and MCP servers, secrets, and experiments that belong to your checkout but not to the shared configuration. Local configuration belongs to one checkout. Instructions and skills that should follow you across every repository (including ones that do not use ai-rulez) go to your home directories instead; see [User-level configuration](user-scope.md). Local configuration has two layers. Both are gitignored unconditionally, even when `gitignore = false`. | Layer | Location | Holds | | ----- | -------- | ----- | | Content tree | `.ai-rulez/local/` | Rules, context, skills, agents, commands and domains | | Config overlay | `.ai-rulez/config.local.toml` | Settings merged onto `config.toml`: presets, profiles, MCP servers, includes, installed skills, and so on | Committed outputs never contain local content, so a teammate who checks out your branch sees only the shared configuration. Generated local files are written for your tools to load, and are kept out of git. #### Content tree ```text .ai-rulez/ ├── config.toml ├── rules/ # shared, committed ├── context/ # shared, committed └── local/ # machine-local, gitignored ├── rules/ ├── context/ ├── skills/ ├── agents/ ├── commands/ └── domains// # same layout; selected by the active profile like shared domains ``` - It mirrors the shared layout and uses the same file formats and frontmatter. - Local domains are selected by the active profile like shared ones. A local profile (in the overlay) may reference local domains. - Local content is root-only: `[[scopes]]` runs never emit local outputs. - Files and directories created through the CLI or MCP tools are owner-only (`0600` / `0700`). - A local skill, agent or command with the same name as a shared one is an error: a local file may never replace a shared one. Skills and commands share one namespace. Local rules and context may reuse a shared name; see [Collisions](#collisions). ##### Generated output Where a local rule or context item lands depends on the preset and the [rules mode](rules.md#rules-mode). Every file below is one the tool loads on its own; a `.local.md` file a tool never reads is not written. - Local rules the preset routes to rule files are written as `/.local`, the same routing shared rules use, so the tool loads them natively. - Everything else (local context, and rules the preset keeps inline) goes to the file the tool loads for machine-local instructions, listed below. It is written only when there is something to put in it. - Local skills, agents and commands are written per item, to the same paths as shared ones (for example `.claude/skills//SKILL.md`). A preset that aggregates them into a shared file, or has no output for them, gets none; `generate` warns once per preset. - Content a preset has no place for (local context with Cursor, say) is not written and is reported in one warning per preset. - Custom providers (`.ai-rulez/providers/`) get no local output at all. | Preset | Split mode (default) | Inline mode | Loaded by | | ------ | -------------------- | ----------- | --------- | | `claude` | Rules: `.claude/rules/.local.md`. Context: `CLAUDE.local.md` | Path-scoped rules: `.claude/rules/.local.md`. Other rules and context: `CLAUDE.local.md` | Claude Code reads `CLAUDE.local.md` after `CLAUDE.md` and the rules folder natively | | `junie` | Rules: `.junie/rules/.local.md`. Context: `.junie/rules/ai-rulez.local.md` | Rules and context: `.junie/rules/ai-rulez.local.md` | Junie loads `.junie/rules/*.md` only on its `AGENTS.md` discovery path: a root `AGENTS.md` combined with `.junie/playbook.md` and every `.junie/rules/*.md`. A `.junie/AGENTS.md` takes precedence and ends the search, and the legacy `.junie/guidelines.md` layout does not load the rules folder, so with either of those the local file is not read. Based on JetBrains' documentation; not tested against Junie | | `cursor` | Rules: `.cursor/rules/.local.mdc`. No place for context | Same | Cursor loads its rules folder | | `devin` | Rules: `.devin/rules/.local.md`. No place for context | Same | Devin loads its rules folder | | `cline` | Rules: `.clinerules/.local.md`. No place for context | Same | Cline loads its rules folder | | `copilot` | Routed rules: `.github/instructions/.local.instructions.md`. Rules Copilot keeps inline (`auto`, `manual`, negated-only globs) and context: `.github/instructions/ai-rulez.local.instructions.md` (`applyTo: "**"`) | Same routing as shared rules; the remainder goes to `ai-rulez.local.instructions.md` | Copilot loads path-specific instructions files | | `antigravity` | Rules: `.agents/rules/.local.md`. Context: `.agents/rules/ai-rulez.local.md` (`trigger: always_on`) | Path-scoped rules: `.agents/rules/.local.md`. Other rules and context: `.agents/rules/ai-rulez.local.md`. With `gemini` also enabled and no explicit `mode_by_preset`, all rules stay in `ai-rulez.local.md` | Antigravity loads every `.agents/rules/*.md` with a `trigger`; it never reads `GEMINI.local.md` | | `gemini` | Rules and context: `GEMINI.local.md` | Same | Gemini CLI loads it because `.gemini/settings.json` `context.fileName` lists it (written whether or not local content exists). A `context.fileName` you wrote gets `GEMINI.local.md` appended; see [Settings documents shared with you](#settings-documents-shared-with-you) | | `opencode` | Rules and context: `AGENTS.local.md` | Same | OpenCode loads it because `opencode.json` `instructions` lists it (written whether or not local content exists; `./AGENTS.local.md` counts as the same entry) | | `xum` | Rules and context: `AGENTS.local.md` (shared with `opencode`) | Same | xum appends `AGENTS.local.md` to `AGENTS.md` | | `pi` | Nothing written, one warning | Same | pi reads no project-local `AGENTS.local.md`; put personal guidance in pi's user config | | `codex` | Rules and context: `AGENTS.override.md` (root only) | Same | Codex loads `AGENTS.override.md` instead of `AGENTS.md` in the same directory, so the file repeats the shared `AGENTS.md` and appends the local sections | | `hermes` | With `agents_md`: `AGENTS.override.md`, as for Codex. Without it: nothing written, one warning | Same | Hermes loads `AGENTS.override.md` instead of `AGENTS.md` in the AGENTS chain; `.hermes.md` (without `agents_md`) beats the chain and has no local counterpart | | `amp` | Nothing written, one warning | Same | Amp has no project-local file; put personal guidance in `~/.config/amp/AGENTS.md` | `AGENTS.override.md` is generated, git-ignored and listed in the local manifest, so it is removed when the local content goes away. Do not edit it: it replaces `AGENTS.md` for those tools and is rebuilt from `AGENTS.md` on every `generate`. It repeats the `AGENTS.md` body that run actually wrote (without that file's generated banner, which keeps a `[header] timestamp` from rewriting it every run). An `AGENTS.override.md` that is not in the local manifest and has no generated banner is yours: it is never overwritten or deleted, `generate` warns naming it, and Codex then reads it instead of the local content. If local content exists but no `AGENTS.md` is produced, the file is not written and `generate` warns. A local rule named `ai-rulez` would map to the same path as the generated `ai-rulez.local.*` root file of `junie`, `antigravity` and `copilot`, so it is written as `ai-rulez-.local` instead. ##### Bookkeeping - **Gitignore.** Local rule files share a folder with committed rules, so one pattern per folder is written (for example `.claude/rules/*.local.*`) before the files themselves. The managed block also lists `.ai-rulez/local/`, `.ai-rulez/config.local.*`, `.ai-rulez/.config.local.*` (lock and temp files) and `.ai-rulez/.generated-manifest.local.json`. - **Symlinked `.gitignore`.** Git does not read a `.gitignore` that is a symbolic link, and ai-rulez never writes through it: every ignore entry goes to a per-project block in `.git/info/exclude` instead. - **Fail closed.** `generate` stops, listing the paths, when a machine-local or secret-bearing output, the `config.local.*` overlay or the `local/` tree would not be git-ignored after the entries are written (for example because a `.gitignore` line such as `!.ai-rulez/local/` un-ignores it). Narrow the rule, or use `--no-local`. - **Per-clone excludes.** Local-only outputs whose names do not contain `.local.` (a local skill's `SKILL.md`, `AGENTS.override.md`, an output of an overlay-defined preset) differ per machine. They are listed in a block of this project's `.git/info/exclude`, keyed by the project's config directory, so they stay out of the shared `.gitignore`. Outside a git repository they fall back to the managed `.gitignore` block. `clean`, or a run with no local inputs left, removes the block. - **Local manifest.** Local outputs are tracked in `.ai-rulez/.generated-manifest.local.json`, not in the committed manifest. A teammate's `generate` never deletes your local files; `clean` removes them through the local manifest. It also records what ai-rulez merged into settings documents you share with it (below), because the server names can come from your overlay. It is ignored like the other local files. - **Permissions.** Generated files that contain a resolved MCP secret are written `0600`, whichever preset produced them. The overlay itself is written `0600`. - **Files from earlier versions.** `AGENTS.local.md` (codex and amp only setups), `.hermes.local.md`, `.junie/guidelines.local.md` and Antigravity's `GEMINI.local.md` were never read by their tools. The first `generate` after upgrading removes them through the local manifest. - **Reserved names.** `*.local.*` in a rules folder is reserved. A hand-written file with such a name is skipped with a warning. ##### Settings documents shared with you `.claude/settings.json`, `.gemini/settings.json`, `opencode.json`, `.mcp.json`, `.agents/settings.json` and `.xum/mcp.jsonc` can hold your own settings beside what ai-rulez writes. ai-rulez records, per document, the MCP server entries, array elements and scalar keys it merged in, each with a digest of the value it wrote (never the value, which may be a secret; a record an older version wrote with the plain value is converted to a digest the next time it is read). Array elements are counted per value: if ai-rulez added one `Bash(git status)` rule, `clean` removes one, and an identical rule you wrote yourself stays. A document it wrote whole is recorded in the committed manifest, a document shared with you or carrying overlay content in the local manifest. - **`clean`** removes exactly those entries, only while they still hold the value ai-rulez wrote, and keeps every other key and the document's formatting. An entry you edited is yours: it stays and `clean` warns once, naming the file and key. A key or object the removal leaves empty is dropped, and a document with nothing else in it is deleted (a document made only of ai-rulez's keys, such as one it created, is therefore deleted by `clean`). This includes an `mcpServers` entry that holds a resolved secret (a header or env value from your overlay), which previously outlived the overlay in a hand-written `.claude/settings.json`. - **`generate`** takes back what an earlier run recorded and this one no longer produces: a preset removed from the config, or an MCP server removed. It works after the overlay is deleted, because the record is the local manifest. `generate --dry-run` lists these as `unmerge:` lines. - **No record.** A document merged by 4.23.0 or earlier, or a fresh clone, has no record. The fallback runs on `clean` only, never on `generate`, and is narrow: it removes an MCP server your config declares by name only when its value equals what the config would render for that document (a hand-written server of the same name with another value stays, with a warning, even if the preset that would write it is off), the ai-rulez self-registration when it equals what ai-rulez writes, a `context.fileName` that exactly equals a value ai-rulez wrote, `AGENTS.local.md` entries of `instructions`, and an OpenCode `$schema` that is the document's only key. A hand-written value that happens to equal what ai-rulez would render is indistinguishable from its own and is removed. A server removed from the config before the first run that records it is not recognized and stays; delete it by hand. - **Hand-written servers.** A server whose name is not in your config is never touched. One with the same name as a configured server is replaced by the configured value on `generate`. - **Gemini `context.fileName`.** A value that exactly equals one of ai-rulez's forms (`["AGENTS.md"]`, `["AGENTS.md", "GEMINI.local.md"]`, `["GEMINI.md", "GEMINI.local.md"]`) is ai-rulez's, with or without a manifest, and is rewritten to the current form (this is how `agents_md` toggles it). Any other value is yours: it is kept, a single string becomes a list, and `GEMINI.local.md` (and `AGENTS.md` under `agents_md`) is appended when missing, without a warning. Only the names ai-rulez added are taken back: `clean` on `"MY.md"` leaves `["MY.md"]` (a list; the string form is not restored), and turning `agents_md` off removes only the `AGENTS.md` it appended. An `AGENTS.md` you listed yourself stays. With `agents_md` off, a list without `GEMINI.md` still gets a warning, because Gemini then ignores the generated file. - **OpenCode.** `AGENTS.local.md` is appended to your `instructions` (`./AGENTS.local.md` counts as the same entry) and taken back by `clean`. `$schema` is written only when ai-rulez creates `opencode.json`, never added to yours. - **Comments (JSONC).** Gemini CLI and OpenCode accept comments in these files, but rewriting would delete them. A document with comments or trailing commas is therefore left untouched: when only the `context.fileName` or `instructions` registration would be written, `generate` warns (and `GEMINI.local.md` or `AGENTS.local.md` is not loaded until you add it by hand); generation does not stop. When MCP servers must be written into such a document, `generate` still stops with a hint, as before. `clean` leaves a commented document alone with a warning. This includes `.xum/mcp.jsonc`, which is JSONC by definition: once you add a comment, ai-rulez can no longer edit it, so the servers it merged stay after a preset or server is removed or after `clean`. It warns once per run; remove those entries by hand. - `clean` does not print the Gemini `context.fileName` advice. ##### Collisions - A local rule that has the same name as a shared rule is allowed; its file is `.local`, so nothing is overwritten. - Two local rules that map to the same file name are disambiguated as `-.local`. - Local skills, agents and commands that collide with a shared item fail `generate` with an error naming both files. #### Config overlay `config.local.toml` sits next to `config.toml`. It is merged onto the shared config in memory at load time and is never written into the shared config. A V3 `config.local.yaml`, `.yml` or `.json` is no longer read and fails the load (see [Migrating to v5](migration-v5.md#upgrade-in-four-steps)). The overlay is skipped for plugin bundles (`generate --plugin`), which are distributable. Create one with `ai-rulez local init`, or let `local set`, `--local` and the MCP `local: true` flag create it on first use. Overlay files are validated against `schema/ai-rules-local.schema.json` (see [Schema Reference](schema.md)); the merged result must also be a valid config. ##### Merge semantics | Key kind | Keys | Rule | | -------- | ---- | ---- | | Scalars | `name`, `description`, `default`, `gitignore`, `compact`, `agents_md`, `codex_skills_dir`, ... | Local value wins | | Lists | Any list not named below (`hooks`, `skill_sources`, `bundle_exclude`, the values of `profiles`, ...) | Local list replaces the shared list | | Tables | `profiles`, `header`, `defaults`, `mcp`, `rules`, `plugin`, `marketplace`, `placement`, `claude`, `lint`, `permissions`, `guard`, `role_manifest`, `lock`, `llm` | Merged per key, recursively; local keys win and lists inside replace the shared list. A table key that is not a table locally (including `null`) is an error | | Other tables | `domains`, `skills`, `okf`, `usage`, `telemetry`, `codex` | Merged per key like the tables above (a non-table local value replaces the shared one instead of erroring) | | `presets` | | Ordered union: local entries are appended, duplicates collapse (a table beats a bare name; two tables merge, the local one wins). `"!name"` drops a shared preset | | Named lists | `mcp_servers`, `plugins`, `includes`, `installed_skills`, `marketplaces`, `scopes`, `roles`, `verifiers` | Entries merge by `name` (`scopes` by `path` when unnamed). A local entry with `remove = true` deletes the shared entry; a new name appends | | `builtins` | | Local value replaces the shared one | | `version` | | Must be a string and equal the shared version if set | Other rules: - Unknown keys are an error. Error messages name keys and entry positions, never values. - Dropping a preset or removing an entry that the shared config does not have produces a warning. - `remove` is not valid on preset tables; use `"!name"`. - Every named-list entry needs a `name` (a `scopes` entry a `name` or `path`); a duplicate local entry, or one that matches several shared entries, is an error. - `[llm]` and `[telemetry]` network, credential and `service_name` keys are ignored when they come from the overlay (user scope only), and a local include or `local_override` that points outside the project is allowed here but not in the committed `config.toml`. See the [trust model](trust-model.md). ```toml # .ai-rulez/config.local.toml presets = ["codex", "!cursor"] # add codex, drop cursor default = "dev" [profiles] dev = ["backend", "my-local-domain"] [[mcp_servers]] name = "github" # merges onto the shared "github" server transport = "stdio" [mcp_servers.env] GITHUB_TOKEN = "ghp_example" [[includes]] name = "team-rules" remove = true # drop a shared include on this machine ``` #### Managing local configuration ##### Commands The `ai-rulez local` command edits the overlay: | Command | Action | | ------- | ------ | | `local init` | Create a commented `config.local.toml` skeleton (gitignored) | | `local show` | Print every key the overlay sets and the shared value it replaces | | `local set [value]` | Set a key | | `local unset ` | Remove a key | | `local path` | Print the overlay file path | ```bash ai-rulez local set default dev ai-rulez local set presets '["codex", "!cursor"]' ai-rulez local set mcp_servers.github.command npx printf %s "$TOKEN" | ai-rulez local set mcp_servers.github.env.GITHUB_TOKEN --stdin ai-rulez local set 'mcp_servers["foo.bar"].command' npx ai-rulez local show --format json ``` - **Value parsing.** The value is parsed as a TOML literal and falls back to a plain string. Env and header values and known text fields (`url`, `command`, `source`, `path`, `ref`, `description`, `name`, `transport`, `default`, `*_version`) at their real positions are always strings. `--string` forces a string. - **Secrets.** Put them on stdin with `--stdin` (the value is stored as a string) rather than on the command line, where they end up in shell history. - **Dotted names.** Write a segment containing a dot in brackets with double quotes: `mcp_servers["foo.bar"].command`. - **Redaction.** `local show` withholds values by default. Only an allowlist of keys known to hold no credentials (`name`, `description`, `default`, `presets`, `gitignore`, `compact`, `builtins`, `profiles.*`, `defaults.effort*`, `defaults.omit_agent_fields`, `rules.mode*`, `header.style|hashes|timestamp`, `mcp.self_server`, `mcp.self_server_version`, and `transport`, `enabled`, `remove` of list entries) is printed, and only when the value has the expected type. Everything else shows its key path and ``. `--reveal` prints everything and may print secrets. - **Safety.** Every write takes a file lock, ensures the ignore entries first, and validates the merged config. If validation fails the previous file is restored. - **Location.** The subcommands honour the global `--config` and `--config-dir`. `profile list`, `include list` and `skill list` show the shared layer only and print how many local entries `config.local.*` adds. Use `local show` to see them. ##### Content and config CRUD with `--local` The `--local` flag on CLI commands, and `local: true` on the MCP tools, redirect a change to the local layer. | Target | Commands | | ------ | -------- | | `.ai-rulez/local/` content | `add rule\|context\|skill\|agent\|command`, `remove rule\|context\|skill\|agent\|command`, `list rules\|context\|skills\|agents\|commands` | | Overlay | `profile add\|remove\|set-default`, `include add\|remove`, `skill install\|remove` | ```bash ai-rulez add rule local-paths --local # .ai-rulez/local/rules/local-paths.md ai-rulez add context local-env-notes --local ai-rulez add skill my-debug-skill --local ai-rulez add rule team-notes --local --domain backend # .ai-rulez/local/domains/backend/rules/ ai-rulez profile add dev backend my-local-domain --local ai-rulez include add scratch ../scratch-rules --local ``` - `--local --domain ` writes to `.ai-rulez/local/domains//`, creating a local domain. - Local profiles may use shared or local domains. - Removing a shared include or installed skill with `--local` writes `remove = true` to the overlay. Shared profiles cannot be removed locally: `profile remove --local` of a shared profile is an error. - `domain add|remove` and the MCP `create_domain`, `delete_domain`, `list_domains` tools have no local form; a local domain exists once local content is written to it. #### Drift guard The overlay can change files that the team shares (an overlay MCP server adds a block to `.mcp.json`, say). To make sure a local value never reaches a tracked file, `generate` renders the shared view in parallel with the merged view and classifies every output: | Class | Meaning | Result | | ----- | ------- | ------ | | local-only | Only the merged render produces the file | Written; kept out of git | | drift | Both renders produce the file with different content | Written only if the file is git-ignored and untracked | | suppressed | Only the shared render produces the file (the overlay dropped it) | Left alone | A run is **blocked** and exits non-zero, writing nothing, when: - a drift file is tracked by git, or is not ignored, or - a local-only file is tracked by git, or - git cannot answer which files are tracked (every candidate then counts as tracked). The error names paths only, never content. If the shared render itself fails, the run fails closed. Ways forward: - Git-ignore the files, or untrack them. - `generate --no-local` generates the shared view only (the view a teammate sees). - `generate --allow-local-drift` writes anyway. This can put non-secret overlay values into tracked files. It does not bypass the secret guard: a generated MCP config that would carry resolved secrets (env, headers, URL credentials, secret flags) is still refused unless it is git-ignored. It is CLI-only: the MCP server cannot bypass the guard. `generate --dry-run` prints the plan with these lines, and exits non-zero when it would be blocked: ```text local-only: .claude/rules/scratch.local.md drift: .mcp.json allowed: .gemini/settings.json (drift, but git-ignored) suppressed: .cursor/mcp.json blocked: .mcp.json (shared output is tracked and would change) ``` Header `Source-Hash` values of shared outputs come from the shared view, so a teammate regenerating sees no hash churn; local-only outputs carry a hash of their local inputs. The overlay enters that hash only in redacted form. #### `--no-local` `--no-local` ignores both layers. It is accepted by `generate`, `validate` and `tokens`; `verify` has no flag because it always checks the shared view. A `--no-local` run neither deletes your existing local files nor removes their ignore entries. Use it in hooks and CI so results do not depend on one machine's overlay; see [Git hooks](poly-hooks.md). #### Workflow 1. Add local content: `ai-rulez add rule local-paths --local`, or edit the overlay with `ai-rulez local`. 2. Edit the source file under `.ai-rulez/local/rules/`. 3. Run `ai-rulez generate`. Under the default split mode this writes `.claude/rules/local-paths.local.md` (and the equivalent file for each configured preset), and ensures the ignore entries exist. `CLAUDE.local.md` appears only for local context or rules kept inline. 4. Commit as usual. Local files and the local manifest stay out of the commit. Removing a file under `.ai-rulez/local/` and regenerating drops the corresponding output. #### Security notes - The overlay and local tree may hold secrets. They are written owner-only, and ignore entries are written before the files themselves; writers fail closed if the entries cannot be written. - The `local show` default withholds values; `read_config` over MCP returns key paths only. - The overlay is hashed into generated headers only in redacted form: env and header values and args are replaced, and URLs (including include and skill sources) lose their credentials, query and fragment; scheme, host and path still contribute to the hash. - `--allow-local-drift` is the one way to write non-secret overlay-derived values into tracked files. Do not use it in shared scripts. It never writes resolved secrets into an MCP config that is not git-ignored. #### Related - [Configuration Reference](configuration.md#local-overlay) - overlay in the config reference - [CLI Commands](cli.md#local-configuration) - `ai-rulez local` and `--local` - [MCP Server](mcp-server.md) - the `local` tool parameter - [Rules](rules.md) - rules mode and rule files ## Catalog Source: https://goldziher.github.io/ai-rulez/catalog/ `ai-rulez catalog` describes everything a repository defines: rules, context, skills, agents, commands and checks, with owner, version, size, the digest `ai-rulez.lock` pins, the roles that keep each item and the lock status. It prints a table, JSON for tools, or a static website. #### JSON ```bash ai-rulez catalog --format json # version 1 (default) ai-rulez catalog --format json --schema-version 2 # version 2 ``` Version 1 ([`schema/catalog.v1.schema.json`](https://github.com/Goldziher/ai-rulez/blob/main/schema/catalog.v1.schema.json)) stays the default for one minor release so existing consumers keep working. Version 2 ([`schema/catalog.schema.json`](https://github.com/Goldziher/ai-rulez/blob/main/schema/catalog.schema.json)) is a strict superset of the version 1 fields, plus: | Field | Meaning | | ----- | ------- | | `generated_by`, `project` | tool name and version; project name, description and lock tree digest | | `items[].ref` | stable key `kind/domain/id` (domain `-` when none); a repeated key gets `#2`, `#3` | | `items[].description`, `source` | description from the frontmatter (verbatim; a consumer escapes it); `local` or `include` | | `items[].load_cost` | `listing_tokens` (what the harness always lists: skill name and description), `body_tokens` (loaded on use), `resource_tokens` and `resources` (bundled files) | | `items[].lint` | `status` (`ok`, `warn`, `error`), counts and findings, from the same engine as `validate`; absent when lint could not run | | `items[].excerpt` | first 2 KiB of the body, plain text; off with `--include-excerpt=false` | | `items[].approval` | `null`, or `{required, status, reviewers, assurance, expires}` when `[governance]` requires approval of the item or the lock records one; see [Approvals](approvals.md) | | `items[].eval`, `items[].usage` | with `--with-eval` / `--with-usage`: a skill's recorded eval result and use count, see [Eval and usage](#eval-and-usage) | | `mcp_servers` | the project's MCP servers: `ref`, `name`, `transport`, `command_basename`, `enabled`, `profiles`, `pinned` (exact version or digest in a package-runner launch; `null` when not applicable), and the *names* of `env` and `headers` entries, each marked `literal` or with the variable it references (`ref`); `warnings` flags an unpinned launch or a credential written as a literal | | `edges` | dependencies between items: `{from, to, kind: "uses"}`, where `from` names the skill `to` in its `skills:` frontmatter (both are item refs); roles are not edges, see `items[].roles` | | `lint` | project totals, counts per code, and the findings no item owns | | `notes` | why a section is missing or narrowed | MCP servers never expose arguments, URLs, env values or header values: a catalog is published, and those carry launch secrets and internal hostnames. Only the executable's file name and the names of env and header entries appear. Paths are relative to the configuration directory; no absolute path of the machine appears. A consumer must refuse a `schema_version` it does not know. The MCP `catalog` tool prints version 1, equal to the CLI default. #### Eval and usage ```bash ai-rulez catalog --format json --schema-version 2 --with-eval --with-usage ai-rulez catalog --html site/ --with-eval=path/to/eval-results.json --with-usage=path/to/usage.jsonl ``` Both are off by default. Without a value they read the project's own files, `eval-results.json` in the configuration directory and `local/usage.jsonl` (the log the usage hooks write); `--with-eval=FILE` / `--with-usage=FILE` name another file (a file called `default` needs `./default`). A missing file is not an error: the catalog's `notes` say `eval-results.json not found: eval fields are omitted`, and nothing is invented. A file that cannot be parsed is an error. Only an allowlist of aggregates is copied: - `eval`: `cases` (scored), `pass_rate`, `passing`, `ablation_delta`, `trigger_precision`, `trigger_recall`, `stale` (the skill's lock digest differs from the one the run recorded), `verified` (the record carries a valid signature of this machine's key; a result committed from another machine is shown but unverified) and `date`. - `usage`: `invocations` and `last_seen` (a day). Sessions, harnesses, outcomes, feedback and notes never enter the catalog. The log names a skill by id only, so the use of two skills that share an id is left out rather than guessed (the notes say so). The overview table gains an Eval and a Uses column when any skill has the data; each item page gets an Eval and a Usage table. #### Dependency graph The site has a `graph.html` page: items that name a skill in their `skills:` frontmatter (agents, skills, rules) drawn as an SVG, left to right, so an item sits to the left of the skills it uses (longest path layering; roles are drawn in a first column with the items they keep). It is a hand-laid, deterministic drawing: integer coordinates, no script, no external library, no `url()` references, and the same bytes for the same catalog. Every node links to its item page. The same edges are listed in a table below the drawing, which is the accessible alternative to the picture. A name resolves to the skill of the same domain, else to the root skill, else to the only skill of that name. A name that matches nothing, or several skills in other domains, is left out and said in `notes`: it is never guessed. A dependency loop is drawn dashed and listed. A graph over 150 items is shown as the table only, and role edges are left out past 300. #### Comparing catalogs ```bash ai-rulez catalog diff main # a revision against the current project ai-rulez catalog diff v5.0.0 HEAD --format json ai-rulez catalog diff before.json after.json --exit-code ``` Prints what was added, removed or changed between two catalogs: items (matched by `ref`; a change lists the fields that differ: digest, description, owner, version, path, source, delivery, listing/body/resource tokens, lint status, approval status, roles), MCP servers (transport, command, pin status, env and header names), roles, dependency edges and the lint totals. JSON output validates against `schema/catalog-diff.schema.json`. Each argument is a catalog JSON file (`catalog --format json --schema-version 2`, or a site's `catalog.json`; a `schema_version` other than 2 is refused) or a git revision. A revision is read without touching the working tree: `git archive` writes the tracked files of the configuration directory at that commit into a temporary directory (bounded in file count and size, extracted through a root so nothing lands outside it, symlinks reported and skipped) and a catalog is built from it. With one argument the other side is the current project. Both sides are built from the shared configuration only: the machine-local overlay is left out, and remote includes and installed skills are not resolved (no network), so compare two `catalog.json` files to cover them. Untracked and ignored files are not part of a revision. Both sides are linted against the working tree's repository, so a path the content names (`AR401`) is checked the same way on each side, and the plugin version drift check (`AR961`), which compares with outputs generated on disk, is left out of both. Exit code `0` unless the command could not run (`1`); `--exit-code` makes it `2` when the catalogs differ. Text output escapes control and bidirectional characters from the (possibly third-party) JSON. #### Static website ```bash ai-rulez catalog --html site/ ai-rulez catalog --html site/ --role backend --clean ``` Writes a directory with an overview (search by name, domain, owner, kind and lint status), one page per item and per role, the MCP servers, the dependency graph, the lock status, the lint findings, an About page, `catalog.json` (the version 2 document the pages are rendered from), `assets/catalog.css`, `assets/catalog.js` and `robots.txt`. - **Offline.** All links are relative, so the site works from `file://` and under any URL path. Nothing is fetched: no fonts, CDN, analytics or `fetch`. Every page is complete without JavaScript; the script only adds filtering and copy buttons. - **Reproducible.** The same input gives the same bytes: no timestamps, no absolute paths, sorted everywhere. The footer shows the tool version and the sha256 of `catalog.json`, not a date. Regenerate and diff to detect a tampered hosted copy. - **Escaped.** Every string from the repository goes through `html/template` contextual escaping. Links are built from slugged keys, never from source text; source URLs are text. Invisible and direction-changing characters (bidi controls, zero-width characters) are replaced with U+FFFD and the item is marked "contains hidden characters". Bodies are shown as plain text in `
`, never rendered.
- **Content-Security-Policy.** Each page carries `default-src 'none'; img-src 'self' data:; style-src 'self';
  script-src 'self'; base-uri 'none'; form-action 'none'` in a meta tag, and no inline script or style. A host
  should send the same as response headers plus `frame-ancestors 'none'`, which a meta tag cannot set.
- **Output directory.** It must be new, empty or contain the marker `.ai-rulez-catalog` (written by an earlier run,
  listing each file it wrote with its SHA-256). `--clean` removes only a listed file that has the shape of a
  site file (`index.html`, `items/`, `roles/`, `assets/`, `catalog.json`, ...) and still holds the recorded bytes;
  without the marker the run is refused, so `--html .` cannot overwrite a project, and a directory with a `.git` or
  `.ai-rulez` folder is always refused. The marker is written before the files, so an interrupted run leaves the
  directory marked. Writes never follow a symlink out of the directory.
- **Secrets.** The run is refused when the secret scanner (`AR001`) flagged an item and the site would publish its
  excerpt or description; remove the secret, or pass `--allow-findings AR001` (discouraged). The published description and excerpt are also scanned
  directly, so a lint that did not run, a baselined finding or an inline ignore does not let a secret through.

##### Configuration and pages

```toml
[catalog]
title = "Acme catalog"       # site title (--base-title)
include_excerpt = true       # body excerpts (--include-excerpt); default on, off when indexable
exclude_owners = false       # leave owner names out of the JSON and the site (--no-owners)
indexable = false            # let search engines in (--indexable)
max_items_per_page = 200     # overview rows per page (--max-items-per-page); 0 means 200
render_markdown = false      # render excerpts as sanitized Markdown (--render-markdown)
```

Every key is optional and a flag that is given wins. `include_excerpt` and `exclude_owners` also apply to
`--format json --schema-version 2`. The `[catalog]` table can be overridden in the machine-local config.

The overview lists `max_items_per_page` rows per page. All rows are in the one `index.html` (one `` per page),
so browsing, find-in-page and the no-JavaScript view show everything; the script shows one page at a time with
Previous/Next buttons and shows every page while a filter is active. The page size does not change `catalog.json`.

##### Markdown excerpts

`--render-markdown` (or `render_markdown = true`) shows each item's excerpt as formatted Markdown instead of plain
text. The renderer is a sanitizer by construction, not by filtering: the text is parsed as CommonMark and turned into
a tree whose nodes are only paragraphs, headings (shifted to `h3`-`h6`, below the page's own headings), lists, quotes,
code blocks, emphasis, code spans, line breaks and rules. The tree has no field that holds markup, and the page is
built from it by `html/template`, so every character of the body is escaped.

- Raw HTML (block or inline, comments included) is shown as literal text, never interpreted.
- Links are not anchors: the label is kept and the destination is shown as text after it, so a `javascript:`,
  `data:` or remote URL never becomes an `href`. Images show their alt text and the destination as text; nothing is
  loaded.
- Depth is capped at 12 levels and the tree at 4,000 nodes (a notice says so), so a hostile body cannot recurse or
  inflate the page.
- The Content-Security-Policy stays as strict as before; the Markdown view needs no script and no new source.

The excerpt is still the first 2 KiB of the body, so a long document is cut mid-structure. Excerpts are off with
`--indexable` unless asked for.

##### Freshness check

```bash
ai-rulez catalog --html site/ --check
```

Renders the site in memory and compares it with the directory without writing anything. Exit `0` when every file
matches and nothing else is there, `2` when a file is changed, missing or unexpected (the differences are listed),
`1` when the check could not run. A directory that does not exist is drift. Because the output is reproducible, the
same gate detects a tampered hosted copy. Use it in CI to keep a committed site current; `--check` and `--clean` do
not combine. Files are read through the directory only: a symlink in it is reported, never followed.

Flags: `--role R` (items role `R` keeps), `--include-excerpt` (default on), `--indexable` (no `robots.txt`, no
`noindex`; excerpts default off), `--clean`, `--base-title T`, `--allow-findings`, `--render-markdown`, `--max-items-per-page`, `--no-owners`, `--check`, `--with-eval[=FILE]`, `--with-usage[=FILE]`.

A published catalog exposes names, descriptions, owners, token costs and lint findings: treat it like the
configuration directory it describes.

#### Browser tests

`go test ./internal/catalogsite` also runs the site in a real headless Chrome or Chromium when one is installed
(`AI_RULEZ_CHROME` names the executable; `-short` skips them; they skip on their own when no browser is found or it
does not start). The harness speaks the DevTools protocol over `--remote-debugging-pipe`, so it needs no module and no
WebSocket. It opens every page of a rich and of a hostile fixture site from `file://` and requires zero
Content-Security-Policy violations (recorded by a `securitypolicyviolation` listener the protocol injects), no console
error or warning, no uncaught exception, no JavaScript dialog, no inline script or style, and no request outside the
file system. It then filters, pages and navigates (overview, item, graph, MCP pages), and a control test injects an
inline script to prove the policy blocks it, so a zero count is not vacuous. The client itself is also tested against a
fake browser on every machine.

#### Design decisions

- **Builder stays in `internal/govview`.** The issue names `internal/catalog`; the builder already lives in
  `internal/govview` next to the roles and lock views that the CLI and the MCP tools share, and renaming would touch
  every caller for no behaviour change. The renderer is a separate package, `internal/catalogsite`.
- **Dual emit.** Version 1 is the default and `--schema-version 2` opts in (the issue's proposed answer). The HTML
  command always builds version 2.
- **Excerpt default.** On locally, off with `--indexable` (the issue's proposal); `--include-excerpt` overrides.
- **Secret check reuses `AR001`.** No new rule code.
- **Lint at the configured level.** Findings come from the in-process lint engine with the project's `[lint]`
  settings, not the strict level.
- **No `catalog-data.js`.** The pages are server-rendered and the filter reads the table rows, so a second copy of the
  data is not needed; `catalog.json` is the machine contract.
- **Approval.** The overview has an Approval column (the status, or `not required`); the item page shows the status with
  `(required)`, the reviewers, their assurance level and the expiry, escaped like every other value.
- **Pagination is presentational.** All rows are in `index.html` in groups of `max_items_per_page`; the script pages them. Real per-page files would break filtering across pages and find-in-page.

#### Not yet built

`--no-lint-messages`, `--link-sources`, `--single-file`, a `catalog diff` page and approval or signature attestations
in the diff remain from the design.

## Monorepo

Source: https://goldziher.github.io/ai-rulez/monorepo/

For large projects with multiple teams, organize your `.ai-rulez/` configuration using domains and profiles to provide relevant context while avoiding overwhelming AI assistants with irrelevant information.

This guide outlines best practices for managing AI context at scale.

---

#### The Core Strategy: Domain-Based Organization

The most effective strategy for large projects is to organize related rules, context, and skills into domains. Each domain represents a team, service, or feature area.

!!! success "The Goal: High-Relevance, Low-Token Context"

    By organizing into domains and using profiles, you provide your AI assistant with only the context it needs for the task at hand. A developer working on the frontend gets frontend-specific rules and context, while the backend developer gets API patterns and database standards. This results in faster, more accurate, and more relevant AI responses.

!!! info "Domains and Profiles"
    - **Domains** organize content by team, service, or feature (`backend`, `frontend`, `mobile`)
    - **Profiles** select which domains are included in generation (`full`, `backend-team`, etc.)

A typical layout might look like this:

```text
my-project/
└── .ai-rulez/
    ├── config.toml              # ⬅️ Main config: presets, profiles, domains
    ├── rules/                   # ⬅️ Shared rules (all teams)
    ├── context/                 # ⬅️ Shared context (all teams)
    ├── skills/                  # ⬅️ Shared skills (all teams)
    └── domains/
        ├── backend/             # ⬅️ Backend team content
        │   ├── rules/
        │   ├── context/
        │   └── skills/
        ├── frontend/            # ⬅️ Frontend team content
        │   ├── rules/
        │   ├── context/
        │   └── skills/
        └── qa/                  # ⬅️ QA team content
            └── rules/
```

When you run `ai-rulez generate --profile backend`, it includes root content plus backend-specific content.

---

#### Best Practice: Root Configuration

Your root `.ai-rulez/` should contain high-level, cross-cutting concerns that apply to all teams.

!!! tip "What to put in Root Content"

    - **System Architecture:** High-level overview of the tech stack and service interactions
    - **Cross-Cutting Concerns:** Rules that apply everywhere (security, logging, error handling)
    - **General Standards:** Code quality, testing, git workflow, deployment processes
    - **Base Skills:** Shared AI skills used by all teams

**`.ai-rulez/config.toml`:**

```toml
version = "5.0"
name = "My Full-Stack Project"
description = "Microservices project with React frontend and Go backend"

presets = ["claude", "cursor", "gemini"]
default = "full"
gitignore = true

[profiles]
full = ["backend", "frontend", "qa"]
backend = ["backend", "qa"]
frontend = ["frontend", "qa"]
qa = ["qa"]
```

**`.ai-rulez/context/architecture.md`:**

```markdown
# System Architecture

This is a microservices project with:

- Go backend services
- React web frontend
- PostgreSQL databases
- Kubernetes orchestration
```

**`.ai-rulez/rules/security.md`:**

```markdown
---
priority: critical
---

# Security Standards

- All secrets use environment variables
- Validate all user input
- Use HTTPS for external APIs
```

**`.ai-rulez/rules/git-workflow.md`:**

```markdown
---
priority: high
---

# Git Workflow

- Feature branches from main
- Squash commits before merge
- All PRs require code review
```

#### Best Practice: Domain-Specific Content

Domain directories contain specific, detailed context for that team or service.

!!! tip "What to put in Domain Content"

    - **Technology Patterns:** Concrete examples for frameworks and libraries
    - **Domain Logic:** Business rules specific to that service or feature
    - **Domain Skills:** Specialized AI prompts for that team's expertise

**`.ai-rulez/domains/backend/rules/database.md`:**

```markdown
---
priority: critical
---

# Database Standards

- Use prepared statements to prevent SQL injection
- Always add migrations for schema changes
- Index foreign keys
```

**`.ai-rulez/domains/backend/rules/api-design.md`:**

```markdown
---
priority: high
---

# API Design

- Follow RESTful principles
- Use consistent error responses
- Version APIs from the start
```

**`.ai-rulez/domains/backend/context/architecture.md`:**

```markdown
# Backend Architecture

## Services

- API Gateway (Go, port 8000)
- User Service (Go, port 8001)
- Product Service (Go, port 8002)
- Order Service (Go, port 8003)

## Database

- PostgreSQL 14+
- Replication enabled
- Automated daily backups
```

**`.ai-rulez/domains/backend/skills/database-expert/SKILL.md`:**

```markdown
---
priority: high
description: "Database design and optimization specialist"
---

# Database Expert

You are an expert in PostgreSQL with knowledge of:

- Schema design and normalization
- Query optimization
- Performance tuning
```

**`.ai-rulez/domains/frontend/rules/components.md`:**

```markdown
---
priority: high
---

# Component Guidelines

- One component per file
- Use TypeScript for type safety
- Write unit tests for all components
- Use composition over inheritance
```

**`.ai-rulez/domains/frontend/context/design-system.md`:**

```markdown
# Design System

## Color Palette

- Primary: #3B82F6
- Secondary: #8B5CF6
- Neutral: #6B7280

## Typography

- Headings: Inter Bold
- Body: Inter Regular
```

#### Best Practice: Profile Selection

Design profiles to match your team structure and workflows.

```toml
[profiles]
# Full platform: all content
full = ["backend", "frontend", "qa", "devops"]

# Team-specific
backend-team = ["backend", "qa"]
frontend-team = ["frontend", "qa"]
qa-team = ["qa"]
devops-team = ["devops"]

# Full-stack developers
full-stack = ["backend", "frontend", "qa"]

# CI/QA environment
ci-cd = ["backend", "frontend", "qa", "devops"]
```

#### Best Practice: For Monorepos

If using multiple `.ai-rulez/` directories in a monorepo, use the `--recursive` flag:

```bash
# Process all .ai-rulez/ directories recursively
ai-rulez generate --recursive
```

**Root configuration** (`/.ai-rulez/config.toml`):

```toml
version = "5.0"
name = "Monorepo Platform"

presets = ["claude", "cursor"]
default = "full"

[profiles]
full = ["shared"]
```

**Service-specific** (`/backend/.ai-rulez/config.toml`):

```toml
version = "5.0"
name = "Backend Service"

presets = ["claude"]
default = "backend"

[profiles]
backend = ["api", "database"]
```

By combining domain organization with thoughtful profile design, you can create scalable, maintainable configurations that grow with your project.

---

#### Scoped Rule Files

For `[[scopes]]` (see [Configuration](configuration.md#scopes)), root files such as `CLAUDE.md` stay in the scope directory, but native rule files are written to the **root** rules folder, because tools read rules folders at the workspace root only. Each scope's files get the scope path as a qualifier and as a glob prefix:

| Preset | Scoped rule file for `packages/api`, rule `style` |
| --- | --- |
| Claude | `.claude/rules/packages-api/style.md` |
| Cursor | `.cursor/rules/packages-api/style.mdc` |
| Copilot | `.github/instructions/packages-api/style.instructions.md` |
| Devin, Cline, Junie | `/packages-api--style.md` |
| Antigravity | `.agents/rules/packages-api--style.md` |

Globs in a scope's rules are relative to the scope root: a rule with `paths: ["**/*.go"]` gets `packages/api/**/*.go`, and a rule without globs applies to `packages/api/**`. A rule with only negated globs also gets `packages/api/**`. A glob that climbs out of the scope with `..` (also inside braces) skips that rule for the scope with a warning. `auto` and `manual` rules keep their mode. With `[rules] mode = "inline"` only path-scoped items move to the root folder; everything else stays in the scope's root file. Two files that map to the same path (names compare case-insensitively) do not fail: the source that sorts later is written as `-<6 hex>` with a warning (see [File names](rules.md#file-names)). Generation fails only when two scopes produce the same qualifier (for example `packages/api` and `packages-api`); rename one of the scope paths.

Limitations:

- Root files left in a scope (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, ...) are read by tools that load nested files. Copilot (`.github/copilot-instructions.md`) reads its instructions at the repository root only, so rules or context that stay inline in a scope's copy are never loaded; generation warns and names them. Use path-scoped rules or `[rules] mode = "split"` for that preset.
- Domains the root run already renders are not repeated in a scope.
- Presets without split rule files (custom provider specs with a non-split `outputs.rules`) keep a scope's rules in the scope directory; generation warns.

## Examples

Source: https://goldziher.github.io/ai-rulez/examples/

ai-rulez uses a file-based directory structure (`.ai-rulez/`) with TOML configuration. These examples show how to organize your configuration across multiple markdown files and a `config.toml` file.

---

#### 1. Minimal Configuration (Single Team)

The simplest setup for a single team with basic rules.

**`.ai-rulez/config.toml`:**

```toml
version = "5.0"
name = "My Project"

presets = ["claude", "cursor"]
gitignore = true
```

**`.ai-rulez/rules/code-quality.md`:**

```markdown
---
priority: high
---

# Code Quality

- Use meaningful variable names
- Comment complex logic
- All tests must pass before merge
```

**`.ai-rulez/context/architecture.md`:**

```markdown
# Architecture

This is a monolithic application with:

- PostgreSQL database
- REST API backend
- React frontend
```

##### Generated Output

```bash
ai-rulez generate
# Creates:
# - CLAUDE.md (context and agent roster)
# - .claude/rules/code-quality.md (the rule, under the default split mode)
# - .cursor/rules/ (one .mdc file per rule and context item)
```

---

#### 2. Multi-Team Configuration with Domains

For projects with multiple teams, use domains to organize team-specific content.

**`.ai-rulez/config.toml`:**

```toml
version = "5.0"
name = "Platform"

presets = ["claude", "cursor", "gemini"]
default = "full"
gitignore = true

[profiles]
full = ["backend", "frontend", "qa"]
backend = ["backend", "qa"]
frontend = ["frontend", "qa"]
qa = ["qa"]
```

**`.ai-rulez/rules/security.md`:**

```markdown
---
priority: critical
---

# Security

- All secrets must use environment variables
- Validate all user input
- Use HTTPS for all external API calls
```

**`.ai-rulez/domains/backend/rules/database.md`:**

```markdown
---
priority: critical
---

# Database Standards

- Use prepared statements to prevent SQL injection
- Always add database migrations
- Index foreign keys for performance
```

**`.ai-rulez/domains/frontend/rules/components.md`:**

```markdown
---
priority: high
---

# Component Guidelines

- One component per file
- Use TypeScript for type safety
- Write unit tests for all components
```

##### Generated Output

```bash
# Generate for backend team
ai-rulez generate --profile backend
# Includes: root rules + backend-specific rules

# Generate for frontend team
ai-rulez generate --profile frontend
# Includes: root rules + frontend-specific rules

# Generate for QA
ai-rulez generate --profile qa
# Includes: root rules + QA-specific rules
```

---

#### 3. Using Skills for Specialized Roles

Create AI skill definitions for specialized tasks.

**`.ai-rulez/skills/code-reviewer/SKILL.md`:**

```markdown
---
priority: high
description: "Code reviewer for quality assurance"
---

# Code Reviewer

You are an expert code reviewer with deep knowledge of:

- Code quality and maintainability
- Testing best practices
- Performance optimization

## Your Responsibilities

1. Review pull requests for correctness
2. Suggest improvements and refactoring
3. Verify test coverage
```

**`.ai-rulez/skills/architecture-expert/SKILL.md`:**

```markdown
---
priority: high
description: "System architecture specialist"
---

# Architecture Expert

You are a system architect specializing in:

- Microservices design
- Scalability patterns
- System reliability

## Your Responsibilities

1. Review architectural decisions
2. Suggest performance improvements
3. Identify technical debt
```

##### Usage in Generated Files

Each skill is written as its own file, for example `.claude/skills/code-reviewer/SKILL.md` and
`.claude/skills/architecture-expert/SKILL.md`. The assistant reads the skill's `description` and loads
the body when the task matches; nothing is added to `CLAUDE.md` for it.

---

#### 4. Complex Project with Multiple Presets

For projects that need different output formats for different tools.

**`.ai-rulez/config.toml`:**

```toml
version = "5.0"
name = "ML Research Platform"
description = "Machine learning platform with team separation"

presets = [
  "claude",
  "cursor",
  "gemini",
  "devin",
  { name = "internal-guide", type = "markdown", path = "docs/AI_DEVELOPMENT_GUIDE.md" },
]
default = "full"
gitignore = true

[profiles]
full = ["research", "infrastructure"]
research = ["research"]
infrastructure = ["infrastructure"]
```

**`.ai-rulez/domains/research/rules/ml-standards.md`:**

```markdown
---
priority: critical
targets: ["CLAUDE.md", "GEMINI.md"]
---

# ML Development Standards

- Use type hints for all functions
- Document mathematical assumptions
- Include reproducibility seeds
```

**`.ai-rulez/domains/infrastructure/rules/deployment.md`:**

```markdown
---
priority: high
targets: ["CLAUDE.md", ".cursor/rules/"]
---

# Deployment Standards

- All changes require review
- Run tests before deployment
- Keep infrastructure as code
```

---

#### 5. Project with Frontmatter and Custom Fields

Markdown files can include YAML frontmatter with custom fields.

**`.ai-rulez/rules/testing.md`:**

```markdown
---
priority: critical
author: qa-team
tags: [testing, quality, ci-cd]
review_date: 2025-01-15
targets:
  - "CLAUDE.md"
  - ".cursor/rules/"
---

# Testing Standards

## Unit Tests

All code changes must include corresponding unit tests.

- Aim for 80%+ code coverage
- Use table-driven tests for Go
- Test both happy path and error cases

## Integration Tests

Test service interactions:

- Database operations
- API endpoints
- External service calls
```

---

#### 6. Monorepo with Shared Rules

For larger projects, reuse configurations across subdirectories.

**`/.ai-rulez/config.toml`** (Root config):

```toml
version = "5.0"
name = "Platform"

presets = ["claude", "cursor"]
default = "full"

[profiles]
full = ["shared"]
```

**`/backend/.ai-rulez/config.toml`** (Backend-specific):

```toml
version = "5.0"
name = "Backend Service"

presets = ["claude", "cursor"]
default = "backend"

[profiles]
backend = ["api", "database"]
```

**`/backend/.ai-rulez/domains/api/rules/endpoints.md`:**

```markdown
---
priority: high
---

# API Endpoint Guidelines

- Use consistent path structure
- Version APIs from the start
- Return consistent error responses
```

##### Generation

```bash
# From root, processes all .ai-rulez/ directories recursively
ai-rulez generate --recursive
```

---

#### 7. Environment-Specific Profiles

Use profiles for different deployment environments.

**`.ai-rulez/config.toml`:**

```toml
version = "5.0"
name = "Web Application"

presets = ["claude"]

[profiles]
development = ["dev-guidelines"]
staging = ["staging-checks", "security-checks"]
production = ["production-critical", "security-hardened", "compliance"]
```

**`.ai-rulez/domains/dev-guidelines/rules/debugging.md`:**

```markdown
---
priority: medium
---

# Development Guidelines

- Enable verbose logging in dev
- Use debug endpoints for testing
- Performance is less critical than clarity
```

**`.ai-rulez/domains/production-critical/rules/reliability.md`:**

```markdown
---
priority: critical
---

# Production Standards

- All deployments require approval
- Monitor error rates in production
- Implement circuit breakers for external services
```

##### Usage

```bash
# Generate for development
ai-rulez generate --profile development

# Generate for production
ai-rulez generate --profile production
```

## Supported Harnesses

Source: https://goldziher.github.io/ai-rulez/harnesses/

ai-rulez ships 52 built-in presets, one per AI coding harness. The table below lists them; `ai-rulez init` also writes the list into a comment in the new `config.toml`. Each preset
writes the harness's own layout: file names, frontmatter, directories and settings documents. For a harness that
is not listed, use a [provider-backed custom preset](configuration.md#provider-backed-presets-full-parity).

The shared `mcp` preset (the root `.mcp.json`) is not a harness and is not in the table.

#### Feature matrix

`yes` means the preset writes that output at project level. `-` means it does not; the reason is under
[Skipped features](#skipped-features). Terms:

- **Rules**: a native rules folder. `-` means rules are inlined into the root instructions file.
- **Commands**: slash commands. A command is written as a skill where the harness has no command folder
  (`codex`, `antigravity`, `devin`, `warp`) and as a prompt or workflow file for `copilot`, `pi`, `cline` and
  `kiro` (`.kiro/prompts`). `claude` still reads `.claude/commands`, but ai-rulez writes commands there as
  user-invocable skills, because Claude Code merges the two.
- **Hooks** and **Perms** (permissions): `yes` renders into a project file, `user` renders only with
  [`generate --user`](user-scope.md) because the vendor reads them from the user configuration only.
  See [Hooks, permissions and settings keys](settings.md) and [Permissions](permissions.md).
- **Checks**: [code-review guidelines](checks.md).
- **User**: supported by `generate --user`; see [User-level configuration](user-scope.md) for the paths.

| Preset | Harness | Rules | Skills | Agents | Commands | MCP | Hooks | Perms | Checks | User |
| ------ | ------- | ----- | ------ | ------ | -------- | --- | ----- | ----- | ------ | ---- |
| `aiassistant` | [JetBrains AI Assistant](https://www.jetbrains.com/help/ai-assistant/) | yes | yes | - | - | yes | - | - | - | - |
| `amp` | [Sourcegraph Amp](https://ampcode.com) | - | yes | - | - | yes | yes | - | yes | yes |
| `antigravity` | [Google Antigravity](https://antigravity.google/docs) | yes | yes | yes | yes | yes | yes | - | - | yes |
| `augment` | [Augment Code](https://docs.augmentcode.com) | yes | yes | yes | yes | yes | yes | yes | yes | yes |
| `baz` | [Baz](https://baz.ai/docs/agents/skills-and-instructions) | - | yes | yes | - | - | - | - | - | - |
| `bob` | [IBM Bob](https://bob.ibm.com/docs) | yes | yes | - | yes | yes | yes | - | - | yes |
| `claude` | [Claude Code](https://code.claude.com/docs) | yes | yes | yes | yes | yes | yes | yes | - | yes |
| `cline` | [Cline](https://docs.cline.bot) | yes | yes | yes | yes | - | yes | - | - | yes |
| `codebuddy` | [CodeBuddy Code](https://www.codebuddy.ai/docs/cli) | yes | yes | yes | yes | yes | yes | yes | - | yes |
| `codebuff` | [Codebuff](https://www.codebuff.com/docs) | - | yes | - | - | yes | - | - | - | - |
| `codewhale` | [Codewhale](https://github.com/codewhale-hq/Codewhale) | yes | yes | - | yes | yes | - | - | - | yes |
| `codex` | [OpenAI Codex](https://developers.openai.com/codex) | - | yes | yes | yes | yes | yes | yes | - | yes |
| `commandcode` | [Command Code](https://commandcode.ai/docs) | - | yes | yes | yes | yes | yes | yes | - | yes |
| `copilot` | [GitHub Copilot](https://docs.github.com/en/copilot) | yes | yes | yes | yes | yes | yes | yes | - | yes |
| `copilot-cli` | [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli) | yes | yes | yes | - | yes | yes | yes | - | yes |
| `cortex` | [Snowflake Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/extensibility) | - | yes | yes | - | - | yes | - | - | yes |
| `crush` | [Crush](https://github.com/charmbracelet/crush) | - | yes | - | - | yes | yes | - | - | yes |
| `cursor` | [Cursor](https://cursor.com/docs) | yes | yes | yes | yes | yes | yes | yes | yes | yes |
| `deepagents` | [Deep Agents Code](https://docs.langchain.com/oss/deepagents/code) | - | yes | yes | - | yes | yes | - | - | yes |
| `devin` | [Devin CLI](https://docs.devin.ai/cli) | yes | yes | yes | yes | yes | yes | yes | - | yes |
| `dsh` | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | - | yes | - | - | - | - | - | - | yes |
| `factory` | [Factory Droid](https://docs.factory.ai) | - | yes | yes | yes | yes | yes | - | yes | yes |
| `gemini` | [Gemini CLI](https://geminicli.com/docs/) | - | yes | yes | - | yes | yes | yes | - | yes |
| `gitlab-duo` | [GitLab Duo CLI](https://docs.gitlab.com/user/gitlab_duo_cli/customize/) | - | - | - | yes | yes | yes | - | yes | yes |
| `goose` | [goose](https://goose-docs.ai) | - | yes | yes | - | yes | yes | - | - | yes |
| `grok` | [Grok Build CLI](https://docs.x.ai/build/overview) | yes | yes | yes | yes | yes | yes | yes | - | yes |
| `hermes` | [Hermes Agent](https://hermes-agent.nousresearch.com/docs) | - | `agents_md` | - | - | - | user | user | - | yes |
| `junie` | [JetBrains Junie](https://junie.jetbrains.com) | yes | yes | yes | yes | yes | user | - | - | yes |
| `kilo` | [Kilo Code](https://kilo.ai/docs) | yes | yes | yes | yes | yes | yes | yes | yes | yes |
| `kimi` | [Kimi Code](https://moonshotai.github.io/kimi-code/) | - | yes | yes | - | yes | user | user | - | yes |
| `kiro` | [Kiro](https://kiro.dev/docs) | yes | yes | yes | yes | yes | yes | - | - | yes |
| `letta` | [Letta Code](https://docs.letta.com) | - | yes | yes | yes | - | yes | yes | - | yes |
| `mimocode` | [MiMo Code](https://mimo.xiaomi.com/mimocode) | - | yes | yes | yes | yes | yes | yes | - | yes |
| `muse` | [Meta Muse Code](https://dev.meta.ai/docs/muse-code) | - | yes | - | - | - | - | - | - | yes |
| `omp` | [oh-my-pi](https://github.com/can1357/oh-my-pi) | yes | yes | yes | yes | yes | - | yes | - | yes |
| `openclaw` | [OpenClaw](https://docs.openclaw.ai) | - | - | - | - | - | - | - | - | yes |
| `opencode` | [OpenCode](https://opencode.ai/docs) | - | yes | yes | yes | yes | yes | yes | - | yes |
| `pi` | [pi](https://pi.dev/docs/latest) | - | yes | yes | yes | yes | yes | - | - | yes |
| `poolside` | [Poolside Pool](https://docs.poolside.ai) | - | yes | - | - | yes | yes | yes | - | yes |
| `qoder` | [Qoder](https://docs.qoder.com/cli/quickstart) | yes | yes | yes | yes | yes | yes | yes | - | yes |
| `qwen` | [Qwen Code](https://qwenlm.github.io/qwen-code-docs/en/) | yes | yes | yes | yes | yes | yes | yes | yes | yes |
| `reasonix` | [Reasonix](https://github.com/esengine/DeepSeek-Reasonix) | - | yes | yes | yes | yes | yes | - | - | yes |
| `replit` | [Replit Agent](https://docs.replit.com/features/agent/overview) | - | yes | - | - | - | - | - | - | - |
| `rovodev` | [Atlassian Rovo Dev](https://www.atlassian.com/software/rovo-dev) | - | yes | yes | - | - | - | - | yes | yes |
| `takt` | [Takt](https://github.com/nrslib/takt) | yes | yes | yes | yes | - | - | - | - | yes |
| `trae` | [Trae](https://docs.trae.ai/ide/rules) | yes | yes | - | - | yes | - | - | - | yes |
| `vibe` | [Mistral Vibe](https://docs.mistral.ai/vibe) | - | yes | - | - | yes | yes | yes | - | yes |
| `warp` | [Warp](https://docs.warp.dev) | - | yes | - | yes | yes | - | - | - | yes |
| `xum` | [Xum](https://xum.coder.com) | - | yes | yes | - | yes | - | - | - | - |
| `zcode` | [ZCode](https://zcode.z.ai/en/docs) | - | yes | yes | yes | yes | user | - | - | yes |
| `zed` | [Zed](https://zed.dev/docs/ai) | - | yes | - | - | yes | - | user | - | yes |
| `zoocode` | [Zoo Code](https://docs.zoocode.dev) | - | yes | - | yes | yes | - | yes | - | yes |

Where each output lands (file names, merged documents, user-level paths) is in the preset's provider spec under
`internal/generator/providers/builtin/.toml` and, for the Go presets (`antigravity`, `baz`, `cline`,
`codex`, `copilot`, `cursor`, `devin`, `gemini`, `opencode`, `xum`), in `internal/generator/presets/`. The
user-level table is in [User-level configuration](user-scope.md#where-things-go).

#### Cross-cutting behavior

- **Shared outputs.** Presets that write the same path (`AGENTS.md`, `.agents/skills/`, the root `.mcp.json`)
  must render identical bytes. `generate` fails and names the presets when they diverge, except for an
  `AGENTS.md` that omits rules because its harness reads them from a rules folder; the complete file is kept
  with a warning. See [AGENTS.md and .agents/skills](agents-md.md).
- **Merged documents.** Settings files that hold your own keys (`.claude/settings.json`, `opencode.json`,
  `.codex/config.toml`, `.qwen/settings.json`, ...) are merged, not replaced. ai-rulez owns the keys, array
  elements and map members it writes, and comments in JSONC, TOML and YAML survive. See
  [Settings document merge behavior](configuration.md#settings-document-merge-behavior).
- **Native MCP environment references.** Where the harness expands `${VAR}`-style references itself, the
  reference is written instead of the secret. See
  [Environment references](configuration.md#mcp_servers).
- **Native tool and model names.** Frontmatter is translated to each harness's own tool names, and bare Claude
  model aliases (`sonnet`, `opus`, `haiku`) are dropped where the harness has no such model.

#### Skipped features

A missing feature is a deliberate skip, with the reason taken from the provider spec or the settings and
permissions docs. Nothing is approximated.

##### Instructions, rules, skills and agents

| Preset | Skipped | Reason |
| ------ | ------- | ------ |
| `aiassistant` | root file | AI Assistant has no root instructions file, so every rule and context item becomes a `.aiassistant/rules/*.md` file |
| `amp` | agents | `.agents/agents` is an Antigravity format Amp does not read; agents are only listed in `AGENTS.md` |
| `baz` | rules folder, commands, MCP | Baz has no repository config file and reads only instruction files from the default branch; it ignores `.claude/commands` and does not read `.claude/rules`. See [Baz](baz.md) |
| `bob` | agents | Bob has no file-based subagents; its modes live in a Roo-style `custom_modes.yaml` aggregate |
| `codebuff` | rules, agents, commands | Codebuff documents only a knowledge file (`AGENTS.md`), skills and MCP |
| `codewhale` | agents | Subagents are TOML profiles with a deny-unknown-fields schema the provider DSL cannot express |
| `crush` | rules folder, agents, commands | Crush has inline guidance (`CRUSH.md`), skills and MCP only |
| `cline` | MCP | Cline has no project MCP file |
| `cortex` | rules folder, MCP | Cortex reads only `AGENTS.md`; project MCP is not documented (servers live in `~/.snowflake/cortex/mcp.json`) |
| `dsh` | MCP | Servers live in the home-level `cordis.patch.yml`, which a project preset cannot express |
| `gemini` | rules folder, commands | Rules stay in the root file (`AGENTS.md`, or `GEMINI.md` with `agents_md = false`); project commands are not generated (see the [plugin runtime](plugins.md) for `commands/.toml`) |
| `gitlab-duo` | skills, rules folder | GitLab documents skills only inside plugins; Duo CLI reads one rules file, `.gitlab/duo/chat-rules.md` |
| `goose` | rules folder, commands | goose has no rules folder; recipes are not slash commands |
| `hermes` | skills | With `agents_md = false` only project context is written (`.hermes.md`); skills come from the shared `.agents/skills`, which `agents_md` (the default) writes |
| `kimi`, `deepagents`, `factory`, `zcode`, `mimocode`, `zoocode`, `warp` | rules folder | No rules folder (or one the harness does not auto-load): rules are inlined into the root file. `kilo` keeps `.kilo/rules` and registers it in `kilo.jsonc` |
| `letta` | rules, MCP | No rules or MCP file the tool documents |
| `muse` | MCP | MCP servers live only in the user `settings.json` |
| `openclaw` | everything but the root file | Only the shared root `AGENTS.md` is written; no rules folder or skills layout can be targeted |
| `poolside` | agents | Subagents are entries of the settings file, which the preset does not generate |
| `replit` | rules, agents, commands, MCP | Replit has `replit.md` and `.agents/skills` only |
| `rovodev` | commands, MCP | Saved prompts need a `prompts.yml` manifest; `.rovodev/mcp.json` is inert until `config.yml` points at it |
| `takt` | MCP | Takt keeps MCP definitions inside workflow YAML. Rules, skills, agents and commands map to facets under `.takt/facets/` |
| `trae` | root file | Trae has no root instructions file; the preset writes rule files, skills and MCP |
| `vibe` | agents | A Vibe agent is a TOML profile whose prompt is a second file, which one output cannot produce |
| `xum` | commands | The tool documents no slash-command file |
| `amp`, `codex`, `commandcode`, `dsh`, `hermes`, `muse`, `opencode`, `pi`, `poolside`, `reasonix`, `rovodev`, `vibe`, `xum` | rules folder | The tool documents no rules folder: rules are inlined into the root instructions file (`AGENTS.md`, `.hermes.md` for `hermes`, `REASONIX.md` for `reasonix`) |
| `aiassistant`, `dsh`, `gitlab-duo`, `hermes`, `muse`, `trae`, `warp` | agents | The tool documents no such file |
| `zoocode` | agents | Custom modes are an aggregated `.roomodes` YAML file, which the provider DSL cannot produce |
| `aiassistant`, `amp`, `copilot-cli`, `cortex`, `deepagents`, `dsh`, `hermes`, `kimi`, `muse`, `poolside`, `trae`, `vibe` | commands | The tool documents no such file |
| `hermes` | MCP | The tool documents no project MCP file |
| `zed` | agents, commands, rules folder | Zed has no rules folder, subagent or command file format; rules go to the root `.rules` file |

##### Hooks

Sixteen presets generate no `[[hooks]]`: `aiassistant`, `baz`, `codebuff`, `codewhale`, `dsh`, `muse`, `omp`,
`openclaw`, `replit`, `rovodev`, `takt`, `trae`, `warp`, `xum`, `zed` and `zoocode`. A preset without hook
support is named in one `generate` warning; ai-rulez emits no hook it could not read from the vendor's
documentation.

`user` in the matrix marks harnesses whose vendor reads hooks (or permissions) only from the user configuration:
`junie` ("ignores project hooks"), `zcode` (ignores project hooks), `hermes`, `kimi` (user `config.toml`
only) and, for permissions, `zed`. They render with `generate --user`. Per-harness event, matcher and timeout
details are in [Hooks, permissions and settings keys](settings.md#more-settings-file-harnesses).

##### Permissions

These harnesses have no documented, committable permission file that can express `[permissions]`, so they get
nothing from it and `generate` names them in a warning (from [Permissions](permissions.md#not-translated)):

| Preset | Reason |
| ------ | ------ |
| `amp` | Current Amp docs removed `amp.permissions`; only whole-tool `amp.tools.disable` remains |
| `pi` | No permission mechanism; `defaultTools` only enables or disables whole tools |
| `cline` | Command permissions are documented only as the `CLINE_COMMAND_PERMISSIONS` environment variable |
| `kiro` | Conflicting documented shapes in `.kiro/agents/*.json`; the workspace permissions file lives outside the repository |
| `factory` | The project settings path is documented as `settings.local.json`, and `commandDenylist` means "ask", not "deny" |
| `crush` | `crush.json` is deprecated for `crushrc`; its schema has `allowed_tools` and no deny |
| `rovodev` | The bash rules location differs between Atlassian's pages and their match order is undocumented |
| `takt` | Only a coarse permission mode (`readonly`, `edit`, `full`), no rules |
| `antigravity` | Only the user-level CLI file is documented; the project path is not |
| `goose`, `junie`, `deepagents`, `warp` | User-level files with whole-tool, allow-only, exact-command or regex semantics that cannot hold the rules without widening them |

Every other preset without a `yes` or `user` in the Perms column has no permission surface in its provider spec.

##### Checks

Only eight harnesses read code-review guidelines from a repository file: `amp`, `augment`, `cursor`,
`factory`, `gitlab-duo`, `kilo`, `qwen` and `rovodev`. Not generated: Augment area grouping and per-area globs,
GitLab `fileFilters`, Takt quality gates, Hermes pre-verify specs, and JetBrains AI Assistant (its self-review
path is a per-user IDE setting). See [Checks](checks.md#where-they-are-written).

##### User scope

`aiassistant`, `baz`, `codebuff`, `replit` and `xum` have no documented user-level location for anything
`generate --user` writes and are reported and skipped. `codebuff` has only an MCP file, which user scope does not
generate. User-level MCP servers are never generated.

## AGENTS.md and .agents/skills

Source: https://goldziher.github.io/ai-rulez/agents-md/

`AGENTS.md` and `.agents/skills/` are the closest thing to a cross-tool convention for project instructions and
Agent Skills. With `agents_md = true`, ai-rulez renders them once and every preset that reads them stops writing its
own copy. The flag is on by default: `AGENTS.md` is the canonical instruction file and `CLAUDE.md` imports
`@AGENTS.md`. Set `agents_md = false` to get the per-tool files of earlier releases (`migrate v5` pins that value for
a 4.x project so its output does not move).

```toml
agents_md = true    # the default
# agents_md = false # per-tool files
```

What changes:

- Always-on rules and context go into one `AGENTS.md` (plus a nested `/AGENTS.md` per `[[scopes]]` entry).
- Skills without `targets` go into one `.agents/skills//SKILL.md` tree.
- Presets that read these files stop writing their root file (`GEMINI.md`, `.hermes.md`, ...), their copy of
  `AGENTS.md` and their own skills directory.
- Everything the tool cannot read from the shared files stays per-preset: scoped rules folders, agents, commands,
  MCP files and settings. A rules folder is created only when at least one rule file is written into it.

With `agents_md = false`, `codex`, `opencode`, `xum`, `pi`, `amp` and `baz` already write the same `AGENTS.md`, while `claude`, `gemini`,
`cursor` and the rest each repeat the same content in their own file.

#### Tool support

Research as of 2026-10-03. `V` verified against official docs or source, `P` partial or experimental, `N` not
supported, `?` not verified. Tools are listed in the order of the presets that read the shared files, followed by two
tools that only matter because they can shadow `AGENTS.md`.

| Tool                       | Root `AGENTS.md`                                                  | Nested `AGENTS.md`                                          | Precedence and double-loading                                                                                                                                  | `.agents/rules` | `.agents/skills`                                                     | Agents, commands                                   |
| -------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| Claude Code                | V, only when no `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` exists in cwd or an ancestor (v2.1.277+; all sessions v2.1.281+) | V lazily, when a file in a subdirectory without any `CLAUDE.md` is read | `@AGENTS.md` import in `CLAUDE.md` never loads twice. `CLAUDE.local.md` suppresses `AGENTS.md` unless the user-level `instructionFiles` setting is `claude-md-and-agents-md`. Reads `.claude/AGENTS.md`, not `AGENTS.local.md` | N               | N (`.claude/skills`)                                                 | N (`.claude/agents`, `.claude/commands`)           |
| Codex CLI                  | V                                                                 | V project root down to cwd only; no lazy loading below cwd  | Per directory first of `AGENTS.override.md`, `AGENTS.md`, fallback names; concatenated root-down; `project_doc_max_bytes` 32 KiB                               | N               | V repo-local, cwd up to repo root; `~/.agents/skills`                | ? (`.codex/agents/*.toml`)                         |
| Cursor                     | V                                                                 | V any subdirectory, more specific wins                      | ? Docs call `AGENTS.md` an alternative to `.cursor/rules`; co-presence and deduplication not documented                                                         | N               | V `.agents/skills`, `.cursor/skills`, nested ones anywhere           | V `.cursor/agents`, `.claude/agents`, `.codex/agents`; no `.agents/agents` |
| Copilot cloud agent, CLI   | V                                                                 | V nearest file in the tree takes precedence                 | Cloud agent: all relevant instruction sets are provided, no deduplication. CLI: removes duplicate copies of identical instructions, defines no general precedence | N               | V `.github/skills`, `.claude/skills`, `.agents/skills`               | ? (`.github/agents`)                               |
| Copilot in VS Code         | V chat and agent                                                  | P experimental `chat.useNestedAgentsMdFiles`                | Additive. `CLAUDE.md` needs `chat.useClaudeMdFile`                                                                                                              | N               | V same three locations                                               | ?                                                  |
| Devin                      | V always-on rule; `agents.md` also recognized                     | V subdirectory file is a glob rule `/**`               | Same rules engine as `.devin/rules`                                                                                                                            | N               | V `.agents/skills`, native `.devin/skills`                          | ? workflows                                        |
| Gemini CLI                 | N by default: reads only `GEMINI.md` unless `context.fileName` lists more | V for configured names, hierarchical and on file access | Only names in `context.fileName` load (`.gemini/settings.json`); `@file.md` imports work in `GEMINI.md`                                                          | N               | V `.agents/skills` beats `.gemini/skills` within a tier              | N (`.gemini/agents`, `.gemini/commands` only)      |
| Antigravity                | V                                                                 | V `AGENTS.md`, `GEMINI.md` or `.agents/rules/` in any subdirectory | Cumulative; directory level wins on conflict. 24 KB per file, 20k tokens in aggregate                                                                          | V any directory level, immediate `.md` children only, `trigger` frontmatter | V `/.agents/skills`; legacy `.agent/skills`               | ? workflows are being superseded by skills         |
| Junie                      | V                                                                 | ? not in the fetched docs                                   | `.junie/AGENTS.md` first; else `AGENTS.md` + `.junie/rules/*.md`; else legacy `.junie/guidelines.md`; identical content deduplicated                           | N               | V `.junie/skills`, `.agents/skills`                                  | ?                                                  |
| Cline                      | V                                                                 | ? shipped code reads the root file only                     | Listed beside `.clinerules`, `.cursorrules`; per-file toggles                                                                                                  | N               | V in code (`.agents/skills`); docs list only `.cline/skills` and others | N                                                  |
| opencode                   | V                                                                 | V lazily, nearest file per read                             | Finds the first existing of `AGENTS.md`, `CLAUDE.md`, `CONTEXT.md` and stacks every ancestor copy of it                                                         | N               | V `.agents/skills`, `.claude/skills`, `.opencode/skills`             | N (`.opencode`)                                    |
| Amp                        | V                                                                 | V when the agent reads a file in the subtree                | Per directory `AGENTS.md`, else `AGENT.md`, else `CLAUDE.md`                                                                                                    | N               | V `.agents/skills` in project and parents                            | ?                                                  |
| xum                        | V                                                                 | V                                                           | `AGENTS.md` > `AGENT.md` > `CLAUDE.md`; also `AGENTS.local.md`                                                                                                  | N               | ?                                                                    | ?                                                  |
| pi                         | V                                                                 | V                                                           | Reads `AGENTS.md`; skills from `.agents/skills` (preferred) or `.pi/skills`; MCP in `.pi/mcp.json`                                                            | N               | V `.agents/skills` beats `.pi/skills`                                | ? (`.pi/agents` subagents extension)               |
| Hermes                     | V git root to cwd                                                 | V                                                           | One context type only: `.hermes.md` shadows `AGENTS.md` entirely                                                                                                | N               | V `.hermes/skills`, `.agents/skills`                                 | ?                                                  |
| Zed                        | V                                                                 | N                                                           | One file per worktree root: the first existing of `.rules`, `.cursorrules`, `.devin/rules`, `.clinerules`, `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`. Earlier entries shadow `AGENTS.md` | N               | V `.agents/skills`                                                   | N                                                  |
| Warp                       | V file name must be upper case                                    | V root and current directory                                | `WARP.md` beats `AGENTS.md` in the same directory                                                                                                               | N               | V `.agents/skills` (recommended)                                     | ?                                                  |

Only Antigravity reads `.agents/rules`; ai-rulez uses it for that tool alone. Other tools covered in the research
(Kilo Code, Roo Code, Factory, Kimi CLI, Aider) are not ai-rulez presets and are omitted.

Sources, all fetched 2026-10-03: Claude Code memory docs (`agents-md` section) and release notes for v2.1.277;
Cursor docs (`cursor.com/docs/context/rules`, `cursor.com/docs/context/skills`); GitHub docs on repository
instructions and agent skills; the VS Code custom-instructions matrix; Devin docs (`docs.devin.ai`, AGENTS.md and
skills); Gemini CLI docs and source (`settingsSchema.ts`, `memoryTool.ts`); the Antigravity rules and skills pages;
Junie guidelines and agent-skills docs; Cline rules docs and extension source; Amp docs
(`ampcode.com/docs/customize/agents-md`); and source reads of Codex (`agents_md.rs`, skills host roots), opencode
(`instruction.ts`), Hermes (`prompt_builder.py`) and Zed (`prompts.rs`, `agent.rs`). Tools change quickly; re-check a
cell before relying on it.

#### What each preset does

Rules mode is the default `split` unless noted. "Shared" means the one `AGENTS.md` or `.agents/skills` tree.
Paths below are what a project with an always-on rule, an overview context file, a glob rule, auto and manual
rules, a glob-scoped context file, two skills, one agent and one MCP server writes (taken from the test fixtures).

| Preset                         | Reads AGENTS.md                      | Root file                                   | Skills                                         | Written beside the shared files                                                                                                  |
| ------------------------------ | ------------------------------------ | ------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `codex`, `opencode`, `xum`, `pi` | natively                             | shared `AGENTS.md`                          | shared; `.opencode/skills`, `.xum/skills` dropped (`pi` never writes skills under `.pi`) | their own agents, commands and MCP files                                                                                         |
| `amp`                          | natively                             | shared `AGENTS.md`                          | shared                                         | unchanged                                                                                                                        |
| `claude`                       | through `CLAUDE.md` containing `@AGENTS.md` | `CLAUDE.md` is a banner plus `@AGENTS.md` | `.claude/skills` kept (Claude ignores `.agents/skills`) | `.claude/rules/*.md` for non-always-on items, `.claude/agents`, `.mcp.json` (MCP servers), `.claude/settings.json` (only hooks, permissions and managed keys)                              |
| `gemini`                       | through `context.fileName` in `.gemini/settings.json` | `GEMINI.md` not written       | shared                                         | `.gemini/settings.json`, `.gemini/agents/.md`, `.mcp.json`                                                                   |
| `antigravity`                  | natively                             | `GEMINI.md` not written                     | shared                                         | `.agents/rules/*.md` for non-always-on items, `.agents/agents/.md`, `.agents/mcp_config.json`, `.mcp.json`                     |
| `hermes`                       | natively                             | `.hermes.md` not written                    | shared                                         | `.mcp.json`                                                                                                                      |
| `cursor`                       | natively                             | none                                        | shared                                         | `.cursor/rules/*.mdc` for non-always-on items and glob context, `.cursor/agents/.md`, `.mcp.json`                            |
| `copilot`                      | natively                             | `.github/copilot-instructions.md` not written | shared; `.github/skills` dropped             | `.github/instructions/*.instructions.md` for `applyTo`-scoped items, `.github/agents/.agent.md`                              |
| `devin`                        | natively                             | none                                        | shared; `.devin/skills` dropped                | `.devin/rules/*.md` for non-always-on items and glob context, `.devin/agents/.md`                                           |
| `cline`                        | natively                             | none                                        | shared; `.cline/skills` dropped                | `.clinerules/*.md` for non-always-on items and glob context, `.cline/agents/.yaml`                                             |
| `junie`                        | natively                             | shared `AGENTS.md`                          | shared; `.junie/skills` dropped                | `.junie/rules/*.md` for non-always-on items, `.junie/agents/.md`                                                             |

Every preset above still writes its MCP file where it did before. Declarative providers (`amp`, `hermes`, `claude`,
`junie`, ...) honor the flag as well. Custom presets and provider specs that are not in the table take no part, and
their `AGENTS.md` or `.agents/skills` output is dropped in favor of the shared copy (see
[Overlapping writers](#overlapping-writers)).

##### How each tool finds AGENTS.md

- **Native readers** (`codex`, `opencode`, `xum`, `amp`, `pi`, `hermes`, `cursor`, `copilot`, `devin`, `cline`,
  `junie`, `antigravity`): nothing to configure.
- **Claude Code** reads `CLAUDE.md`, and only reads `AGENTS.md` itself from v2.1.277 and in every session from
  v2.1.281. ai-rulez therefore keeps `CLAUDE.md` as a generated shim: the generated-file banner followed by
  `@AGENTS.md`. The import works on every version and never loads the file twice.
- **Gemini CLI** loads only the names in `context.fileName`. With the flag on, ai-rulez merges `"AGENTS.md"` (and
  `"GEMINI.local.md"`) into `context.fileName` in `.gemini/settings.json`, keeping existing names and other keys (a
  single-string value becomes a list), and writes the file even when there are no `[[mcp_servers]]`. Gemini replaces its default
  `GEMINI.md` with whatever is configured, which is why `GEMINI.md` is not written.

##### Why copilot-instructions.md, .hermes.md and friends are dropped

Some tools load a single instruction file and pick it by order, so a preset's own root file would hide `AGENTS.md`:

- **Zed** takes the first existing of `.rules`, `.cursorrules`, `.devin/rules`, `.clinerules`,
  `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`. A generated
  `.github/copilot-instructions.md` would hide `AGENTS.md` from Zed, so the `copilot` preset does not write it.
  `CLAUDE.md` and `GEMINI.md` come later and do not shadow it. `.clinerules` is a directory here, which Zed does not
  treat as a rules file.
- **Hermes** loads one context type, and `.hermes.md` shadows the whole `AGENTS.md` chain; the `hermes` preset does
  not write it.
- **Warp** prefers `WARP.md` in the same directory. No preset writes it; do not add one by hand.
- **Junie** prefers `.junie/AGENTS.md`; otherwise it reads the project-root `AGENTS.md` together with `.junie/rules`
  and `.junie/playbook.md`. The legacy `.junie/guidelines.md` layout is no longer written; the root `AGENTS.md` is the
  open-standard file Junie reads.

##### Overlapping writers

If another preset (including a custom provider) also writes `AGENTS.md` or a file under `.agents/skills`, the shared
output wins and the other copy is not written. Without this, the last preset in name order would overwrite the
shared file.

#### Where rules and context go

The shared `AGENTS.md` always carries always-on rules and context, and anything scoped only by negated globs
(`!gen/**`), which no rules folder can express. What else it carries depends on the presets that rely on it.

| Kind                                      | Rules folder presets (`cursor`, `devin`, `cline`, `claude` and `antigravity` in split mode, `junie` in split mode) | Presets without a folder (`codex`, `opencode`, `amp`, `xum`, `pi`, `hermes`, `gemini`) |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Always-on rule or context                 | `AGENTS.md` only                                                                                                                      | `AGENTS.md`                                                                      |
| Glob-scoped rule or context               | the preset's folder                                                                                                                   | `AGENTS.md` with an `_Applies to_` line                                          |
| Auto rule                                 | the folder                                                                                                                            | `AGENTS.md` with a `_When relevant_` line                                        |
| Manual rule                               | the folder                                                                                                                            | `AGENTS.md`                                                                      |
| Only negated globs (`!gen/**`)            | `AGENTS.md` only                                                                                                                      | `AGENTS.md`                                                                      |

The decision is made once for the whole file, from the configured built-in presets that rely on it:

| Configured preset (rules mode)                         | Scoped items (glob rules, glob context) in `AGENTS.md` | Auto and manual items in `AGENTS.md` |
| ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------ |
| `codex`, `opencode`, `amp`, `xum`, `pi`, `hermes`, `gemini`  | yes                                                    | yes                                  |
| `claude`, `antigravity` (split)                        | no                                                     | no                                   |
| `claude`, `antigravity` (inline)                       | no                                                     | yes                                  |
| `cursor`, `devin`, `cline` (either)                    | no                                                     | no                                   |
| `copilot` (either)                                     | no                                                     | yes, its folder only holds `applyTo` items |
| `junie` (split)                                        | no                                                     | no                                   |
| `junie` (inline)                                       | yes, it writes no rule files                           | yes                                  |

The most inclusive row among the configured presets wins. In `inline` mode `claude` and `antigravity` still write
glob-scoped files, so only auto and manual items move into `AGENTS.md`. `cursor` ignores the mode: it always writes
files. Scoped context is inlined together with scoped rules.

##### Duplication trade-off

The inlining is decided for the file, not per reader. When a preset without a folder is enabled next to a preset
with one, the scoped items appear in `AGENTS.md` and in the folder, so the tool with the folder loads them twice.
Examples: `claude` + `codex`, `cursor` + `codex`, `cursor` + `gemini`, `cursor` + `amp`, `cursor` + `hermes`, and
(auto and manual items only) `claude` in `inline` mode + `cursor`. The research found no cross-file deduplication for
Cursor or Claude Code, and none for the Copilot cloud agent. A mixed setup that wants no duplication should keep to
presets that all have folders, accept the overlap, or leave `agents_md` off.

Machine-local rules are never part of the shared `AGENTS.md`. They keep their per-preset files (for example
`.github/instructions/.local.instructions.md`, `CLAUDE.local.md`, `AGENTS.override.md`). See
[Local Configuration](local-overrides.md#generated-output) for the file each tool loads.

#### Skills

- Skills without `targets` are written once to `.agents/skills//SKILL.md`, with bundled resources, in the
  generic Agent Skills format (`name`, `description`). Codex's `metadata.short-description` is not emitted there.
- **Claude Code** reads `.claude/skills`, not `.agents/skills`, so the `claude` preset keeps writing its own copy.
- Skills that set `targets` stay out of the shared tree: they are written to the per-preset skills directories of the
  presets the targets allow, exactly as without the flag. A skill targeting `codex` goes to Codex's own skills directory (`.agents/skills`, or `codex_skills_dir`), the only
  place Codex reads, so other `.agents/skills` readers see it too; an untargeted skill goes to the shared tree; a skill targeting a preset that is not configured is written nowhere.
- `.agents/skills` is read by Codex, Cursor, Copilot, Devin, Gemini CLI, Antigravity, Junie, Cline, opencode, Amp,
  Hermes, pi, Zed, Warp and others, per the table above.

#### targets

Frontmatter `targets` select the presets or files an item is written for. The shared file changes who an item can
reach:

- A rule or context item is included in `AGENTS.md` when its `targets` name a preset that relies on the file
  (`claude`, `gemini`, `cursor`, `codex`, ...), the root file such a preset replaced (`CLAUDE.md`, `GEMINI.md`,
  `.hermes.md`, `.github/copilot-instructions.md`, by path or base name), or any default
  owner of `AGENTS.md` (`codex`, `opencode`, `xum`, `amp`, `pi`, `junie`), configured or not.
- A target naming an unconfigured preset's root file (for example `GEMINI.md` with only `codex` configured) does not
  select the item for `AGENTS.md`.
- **Widening:** the file is shared, so an always-on rule targeted at a single preset now reaches every tool that reads
  `AGENTS.md`.
- **Folder-only targets:** an always-on rule or context item whose `targets` match only a rules folder or rule file
  path (`.cursor/rules/`, `.claude/rules/`, `.github/instructions/`, `.devin/rules/`, ...) and no `AGENTS.md`
  owner or root file is not in `AGENTS.md`. The preset writes it as a rule file in that folder, exactly as without
  the flag. To limit an always-on item to one tool, target its rules folder path rather than its preset name.
- Skills with `targets` are covered under [Skills](#skills).

#### Scopes, local files and hashes

**Scopes.** Each `[[scopes]]` entry gets a nested `/AGENTS.md` carrying only the scope's own domain content.
A scope's `claude` preset writes `/CLAUDE.md` as a shim with `@AGENTS.md`, so Claude Code imports the nested
file. Gemini CLI reads project settings only, so `.gemini/settings.json` is written at the root and not in scopes;
the root setting is what makes Gemini find nested `AGENTS.md` files. If `gemini` is configured for a scope but not for
the root, Gemini gets no instructions for that scope and `generate` warns; add `gemini` to the root presets.

**Local overrides.** Machine-local content is not part of `AGENTS.md`.

- `CLAUDE.local.md` is still written when local content exists. Claude Code ignores `AGENTS.md` when a
  `CLAUDE.local.md` exists, but `CLAUDE.md` imports it explicitly, so nothing is lost.
- Tools that read an AGENTS chain get local content through the file they load for it. Codex, and Hermes with the
  flag on, load `AGENTS.override.md` instead of `AGENTS.md`, so ai-rulez writes a git-ignored `AGENTS.override.md`
  that repeats the shared `AGENTS.md` and appends the local rules and context. OpenCode lists `AGENTS.local.md` in
  `opencode.json` `instructions`; xum appends `AGENTS.local.md` itself. Amp and pi have no project-local file, so
  their local content is not written and `generate` warns. Claude Code is covered through `CLAUDE.local.md`.
- Gemini CLI loads `GEMINI.local.md` because `.gemini/settings.json` `context.fileName` lists it: ai-rulez writes
  `["AGENTS.md", "GEMINI.local.md"]` (`["GEMINI.md", "GEMINI.local.md"]` with the flag off), whether or not local
  content exists. A `context.fileName` you wrote yourself is kept and `GEMINI.local.md` is appended to it; `clean`
  takes back only the names ai-rulez added.
- Junie and Antigravity load the local context from their rules folders (`.junie/rules/ai-rulez.local.md`,
  `.agents/rules/ai-rulez.local.md`).

**Hashes.** The shared outputs carry one `Content-Hash` (and the project-wide `Source-Hash` with `[header] hashes =
"full"`) computed from the content and the settings that shape the file. It does not depend on the list of presets, rules modes, MCP servers or
plugins, so adding a preset that reads the file leaves the provenance line unchanged unless the file itself changes.
It changes with the inlining decision above, and, only when some rule or context item has `targets`, with the set of
presets relying on the file. `AGENTS.md` and the files under `.agents/skills` share the hash. The `[header] hashes`
modes apply as elsewhere: `content` (the default) writes only `Content-Hash`, `full` both hashes, `none` neither.

#### Turning the flag on and off

```bash
ai-rulez generate        # after editing agents_md in .ai-rulez/config.toml
```

The flag is on unless you set `agents_md = false`; the two lists below describe switching between the modes.

- **On:** files that were only needed by the per-tool layout (`GEMINI.md`, `.hermes.md`,
  `.github/copilot-instructions.md`, `.devin/skills`, ...) are removed
  through the generated manifest. `CLAUDE.md` is rewritten as the shim.
- **Off:** the per-tool files are regenerated and the shared `AGENTS.md` and `.agents/skills` files that no preset
  writes itself are removed. An off, on, off sequence ends where it began, except for the Gemini setting below.
- **`.gemini/settings.json`:** on toggle-off, a `context.fileName` that exactly equals a value ai-rulez wrote
  (`["AGENTS.md"]`, `["AGENTS.md", "GEMINI.local.md"]`) is rewritten to `["GEMINI.md", "GEMINI.local.md"]`, whether or
  not ai-rulez wrote the whole file. In a list you authored, only the `AGENTS.md` ai-rulez appended is removed; if the
  value lists `AGENTS.md` but not `GEMINI.md`, `generate` warns so you can add it yourself. Other keys and existing
  `mcpServers` are untouched.
- **Hand-written files** are never removed. A skill you wrote at `.codex/skills/mine/SKILL.md` or
  `.agents/skills/mine/SKILL.md` survives any number of toggles, because only manifest-tracked files are cleaned up.

#### Caveats

- **Claude Code before v2.1.277** does not read `AGENTS.md` by itself; the `@AGENTS.md` shim covers it. Claude Code
  also drops `AGENTS.md` whenever any `CLAUDE.md` is found, which is why the shim imports rather than relies on
  discovery.
- **Duplication is possible** in mixed setups (see the [trade-off](#duplication-trade-off)). The Copilot cloud agent
  and Copilot in VS Code provide all relevant instruction sets and document no deduplication; Copilot CLI only removes
  identical copies.
- **Size limits** apply to the one larger file: Codex reads at most 32 KiB of project instructions by default
  (`project_doc_max_bytes`), Kimi CLI caps at 32 KiB, and Antigravity reads 24 KB per file and 20k tokens in
  aggregate. Use `compact = true` or move bulk content to skills when the file grows.
- **Cursor** documents neither co-presence nor deduplication of `AGENTS.md` with `.cursor/rules`; the preset keeps
  only non-always-on items in `.cursor/rules` so the same text is not in both.
- **Nested `AGENTS.md`** support varies: Codex reads only from the project root down to cwd, Cursor, Copilot, Amp
  and opencode load nested files; Zed does not; Cline and Junie are unverified.
- **Not verified:** the entries marked `?` in the tool table, and whether Copilot expands the `@AGENTS.md` line of
  the Claude shim when it reads `CLAUDE.md` as agent instructions. Status on 2026-10-04: a `?` means no
  dated research pass has confirmed the behavior. The 2026-10-04 pass re-checked only the Codex `project_doc_max_bytes`
  default (32 KiB, content past it is dropped), skill `paths` in Cursor, and Copilot `excludeAgent`; the other `?` cells
  were not re-researched. In particular `.codex/agents`, `.codex/commands`, `.github/agents` and `.github/commands`,
  which `generate` writes, remain unverified as outputs the tools read.

See also: [Configuration: `agents_md`](configuration.md#agents_md), [Rules and native rules folders](rules.md),
[Local Overrides](local-overrides.md), [Monorepo](monorepo.md).

## llms.txt

Source: https://goldziher.github.io/ai-rulez/llms-txt/

[llms.txt](https://llmstxt.org/) is a markdown file at the root of a site or repository that tells language models
what is there and where to read it. ai-rulez supports it in two ways:

- the `llms-txt` preset renders your rules, context and skills as `llms.txt` (and, on request, `llms-full.txt`) in
  your project;
- this documentation site publishes its own [`/llms.txt`](https://goldziher.github.io/ai-rulez/llms.txt) and
  [`/llms-full.txt`](https://goldziher.github.io/ai-rulez/llms-full.txt).

#### The format

The file is markdown with this shape, in this order:

1. an H1 with the project name (the only required element);
2. a blockquote with a short summary;
3. optional detail paragraphs, with no headings;
4. any number of H2 sections, each a markdown list of `[name](url)` links with an optional `: note` after the link.

An H2 section named `Optional` holds secondary links an agent may skip when it needs a shorter context.

```markdown
# Project

> One paragraph that says what the project is.

## Rules

- [Style](.ai-rulez/rules/style.md): Naming and layout

## Optional

- [Reviewer agent](.ai-rulez/agents/reviewer.md): Reviews pull requests
```

`llms-full.txt` has no fixed format. ai-rulez writes the title and summary, then every page as an H2 section holding
its full text, with the page's own headings moved two levels down.

#### The `llms-txt` preset

```toml
presets = ["claude", "llms-txt"]

[llms_txt]
full = true          # also write llms-full.txt
```

`generate` writes `llms.txt` at the project root. Both files are committed documentation, written verbatim with no
generated-by banner (the format needs the H1 on the first line), and kept current by `generate --check` like every
other output.

| Key | Default | Meaning |
| --- | --- | --- |
| `dir` | project root | Directory the files are written to. Must not be inside `.git` or the configuration directory |
| `title` | `name` | The H1 |
| `summary` | `description` | The blockquote under the title |
| `full` | `false` | Also write `llms-full.txt` |
| `include` | `rules`, `context`, `skills` | Kinds to list. `agents` and `commands` are listed in the `Optional` section |

Each item is one link to its source file under `.ai-rulez/`, relative to the output directory. The note is the
`description` from the item's frontmatter, or else the first prose line of its text, cut at 200 characters. Items are
sorted by name, the project's own content first, then each domain (by name) with the domain named in the note.
Output depends only on the content, so it is byte-stable across runs. A monorepo scope run and a role run do not write
the files: they describe the whole project.

#### Validation

`validate --strict` checks the generated `llms.txt` against the format and reports these codes (see
[Strict validation](strict-validation.md#llmstxt-checks)). `llms-full.txt` is not an index and is not checked.

| Code | Name | Default |
| --- | --- | --- |
| AR9P0 | `llmstxt-title-missing` | error |
| AR9P1 | `llmstxt-summary-misplaced` | warning |
| AR9P2 | `llmstxt-heading-invalid` | error |
| AR9P3 | `llmstxt-link-entry-invalid` | error |
| AR9P4 | `llmstxt-optional-misplaced` | warning |
| AR9P5 | `llmstxt-section-duplicate-or-empty` | warning |
| AR9P6 | `llmstxt-link-target-invalid` | warning |

`ai-rulez validate --explain AR9P3` describes one. They belong to the `llmstxt` analyzer.

#### This site

`docs/llms.txt` indexes every page in the `zensical.toml` nav with a one-line description (the page's `description`
frontmatter, or its first sentence), grouped like the nav. Proposals and the changelog go in the `Optional` section.
`docs/llms-full.txt` is every page concatenated, except the changelog. Both are generated from `zensical.toml` and
`docs/`, checked in, and copied to the site root by the build.

```sh
task docs:llms          # regenerate after editing the docs or the nav
task docs:llms:check    # what CI runs: fails when the files are stale
```

The docs workflow runs the check before building the site, and a Go test runs it with the rest of the suite, so a
docs change without a regeneration fails.

## Hooks and Permissions

Source: https://goldziher.github.io/ai-rulez/settings/

`ai-rulez generate` can write the lifecycle hooks, permission rules and a few other settings keys of a
project into each harness's native settings file, without shipping a plugin. The keys are declared once
in `config.toml` and merged into the files through the same owned-key machinery as MCP servers (see
[Settings document merge behavior](configuration.md#settings-document-merge-behavior)): ai-rulez owns
the entries it writes, and every other key, hook group and rule in those files survives `generate` and
`clean`.

Everything on this page is opt-in. A config without `[[hooks]]`, `[guard]`, `[permissions]` or
`[claude.settings.managed]` renders exactly what it did before.

#### `[[hooks]]`

```toml
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
matchers = { gemini = "run_shell_command", cursor = "Shell" }   # per-harness override
[[hooks.hooks]]
script = "scripts/guard.sh"          # or: command = "..."
timeout = 10                         # seconds
if = "Bash(git push *)"              # claude only

[[hooks]]
event = "SessionStart"
targets = ["claude", "codex"]        # optional: restrict to some harnesses
[[hooks.hooks]]
command = "ai-rulez verify"
status_message = "Checking generated files"
```

A group has the same fields as a [`[[plugin.hooks]]`](plugins.md) group (`event`, `matcher`, and `hooks`
with `command` or `script`, `args`, `timeout`, `async`, `if`, `status_message`) plus two that only exist
here, because the same declaration is rendered for several harnesses. Only `type = "command"` handlers exist here (omit `type`); `validate` and `generate` both reject other types:

- `targets`: the harnesses the group is rendered for. Empty means every harness the group can be
  expressed for. One of the harnesses named in the tables below.
- `matchers`: a matcher per harness, overriding `matcher`. Tool names differ between harnesses (Claude
  Code `Bash`, Gemini CLI `run_shell_command`), so a Claude matcher is copied to the harnesses that use the
  same vocabulary (the list under the event tables) and rewritten token by token for the harnesses whose vendor
  documents its tool names (see [Tool names in matchers](#tool-names-in-matchers)). Where a token has no
  documented name, the group is skipped with a warning rather than guessed. `matchers.` always wins.

Events are written with Claude Code's names. Each harness has its own names and handler fields; the
renderer translates them and refuses to approximate:

| Harness | File | Shape | Timeout |
| ------- | ---- | ----- | ------- |
| `claude` | `.claude/settings.json` (`hooks`) | event, matcher group, handlers | seconds |
| `codex` | `.codex/hooks.json` (`hooks`) | event, matcher group, handlers | seconds |
| `gemini` | `.gemini/settings.json` (`hooks`) | event, matcher group, handlers | milliseconds (converted) |
| `cursor` | `.cursor/hooks.json` (`version` and `hooks`) | event, flat handler entries, camelCase events | seconds |
| `copilot` | `.github/hooks/ai-rulez.json` | event, flat handler entries, `bash`, `timeoutSec` | seconds |

Supported events per harness (the Claude Code name on the left; `-` means the harness has no equivalent
and the group is skipped with a warning):

| Claude Code event | claude | codex | gemini | cursor | copilot |
| ----------------- | ------ | ----- | ------ | ------ | ------- |
| `SessionStart` | yes | yes | `SessionStart` | `sessionStart` | `sessionStart` |
| `SessionEnd` | yes | yes | `SessionEnd` | `sessionEnd` | `sessionEnd` |
| `UserPromptSubmit` | yes | yes | `BeforeAgent` | `beforeSubmitPrompt` | `userPromptSubmitted` |
| `PreToolUse` | yes | yes | `BeforeTool` | `preToolUse` | `preToolUse` |
| `PostToolUse` | yes | yes | `AfterTool` | `postToolUse` | `postToolUse` |
| `PostToolUseFailure` | yes | - | - | `postToolUseFailure` | `postToolUseFailure` |
| `PermissionRequest` | yes | yes | - | - | `permissionRequest` |
| `Notification` | yes | - | `Notification` | - | `notification` |
| `SubagentStart` / `SubagentStop` | yes | yes | - | `subagentStart` / `subagentStop` | `subagentStart` / `subagentStop` |
| `PreCompact` | yes | yes | `PreCompress` | `preCompact` | `preCompact` |
| `PostCompact` | yes | yes | - | - | - |
| `Stop` | yes | yes | `AfterAgent` | `stop` | `agentStop` |
| every other Claude Code event | yes | - | - | - | - |

`UserPromptSubmit` and `Stop` map to Gemini's `BeforeAgent` and `AfterAgent`, the nearest events (Gemini
documents them as firing after a prompt is submitted and once per turn after the final response); they
are not identical, so review them when you target Gemini. A test compares this table with the renderer's
event tables.

Handler fields a harness lacks are never dropped silently when dropping would change behaviour:

- `if` exists in Claude Code and Qoder. A handler with `if` is skipped for every other harness, because
  running it unconditionally would widen it.
- `async` exists in Claude Code, Codex, Qwen, Qoder, Junie and ZCode. A handler with `async = true` is skipped
  elsewhere.
- `args` (exec form) exists in Claude Code, Qoder and Deep Agents. For the others the arguments are
  shell-quoted into the command.
- `status_message` is cosmetic and is omitted where the harness has no equivalent.
- Copilot and Copilot CLI honour an optional `matcher` (a regex tested against `toolName`) on `preToolUse` and
  `postToolUse` only; on any other event a group that sets one is skipped.

Every skipped group is reported once per run as a warning: `[[hooks]] not generated for : ...`.
A preset without hook support gets one warning naming those presets; ai-rulez emits no hook it could not read
from the vendor's documentation. The harnesses that do have hook support are listed in the tables below
(settings files and plugin modules).

##### Scripts

`script` is a path relative to the project root. The command written to the settings file addresses it
through the variable the harness documents for its project root:

| Harness | Command for `script = "scripts/guard.sh"` |
| ------- | ----------------------------------------- |
| `claude` | `"${CLAUDE_PROJECT_DIR}"/'scripts/guard.sh'` |
| `gemini` | `"$GEMINI_PROJECT_DIR"/'scripts/guard.sh'` |
| `codex`, `copilot` | `"$(git rev-parse --show-toplevel)"/'scripts/guard.sh'` |
| `cursor` | `'./scripts/guard.sh'` (Cursor runs project hooks from the project root) |
| every harness of the next section | see the script table there |

A script path may only contain letters, digits, `.`, `_`, `-` and `/`; `validate` rejects anything else (a
space, quote, `$`, backtick, `;`, `&`, `|`, newline or backslash would otherwise be spliced into a shell
command line). Every generated line also single-quotes the path, and a user-scope config directory is
single-quoted with embedded quotes escaped, so neither can inject a command. A `command` is yours and is
copied verbatim.

Unlike `[[plugin.hooks]]`, the script is not copied anywhere: it is part of the repository and must be
committed, which is what makes the hook work in a fresh clone. `validate` rejects a script path that
leaves the project; `validate` reports a script that does not exist (`AR504`) or lacks the
executable bit (`AR505`), and the generated `.claude/settings.json` is covered by `AR501`/`AR502`.

##### More settings-file harnesses

The harnesses below share one renderer driven by a table of vendor facts: the events a vendor documents, the
handler field names, the timeout unit, whether a matcher is honoured and on which events. The output is a
hook group merged into the file like the five above (hand-written hooks stay, `clean` takes back only what was
written), and `generate --user` writes the user-level file. `Layout` says how the document is built.

| Harness | Project file | User file (`--user`) | Layout | Timeout |
| ------- | ------------ | -------------------- | ------ | ------- |
| `copilot-cli` | `.github/hooks/ai-rulez.json` (the file `copilot` writes; both render it identically) | `~/.copilot/hooks/ai-rulez.json` | `version` and `hooks`, flat entries with `bash`, `timeoutSec` | seconds |
| `factory` | `.factory/hooks.json` | `~/.factory/hooks.json` | events keyed at the document root, matcher groups | seconds |
| `antigravity` | `.agents/hooks.json` | `~/.gemini/config/hooks.json` | named hook groups; ai-rulez owns the group `ai-rulez` | seconds |
| `qwen` | `.qwen/settings.json` (`hooks`) | `~/.qwen/settings.json` | matcher groups | seconds |
| `augment` | `.augment/settings.json` (`hooks`) | `~/.augment/settings.json` | matcher groups | milliseconds (converted) |
| `codebuddy` | `.codebuddy/settings.json` (`hooks`) | `~/.codebuddy/settings.json` | matcher groups | seconds |
| `qoder` | `.qoder/settings.json` (`hooks`) | `~/.qoder/settings.json` | matcher groups, exec-form `args` | seconds |
| `commandcode` | `.commandcode/settings.json` (`hooks`) | `~/.commandcode/settings.json` | matcher groups | seconds |
| `letta` | `.letta/settings.json` (`hooks`) | `~/.letta/settings.json` | matcher groups | milliseconds (converted) |
| `gitlab-duo` | `.gitlab/duo/hooks.json` (`hooks`) | `~/.gitlab/duo/hooks.json` | matcher groups | seconds |
| `devin` | `.devin/hooks.v1.json` | `hooks` of `~/.config/devin/config.json` | events keyed at the document root, matcher groups | seconds |
| `grok` | `.grok/hooks/ai-rulez.json` | `~/.grok/hooks/ai-rulez.json` | `hooks`, matcher groups | seconds |
| `bob` | `.bob/settings.json` (`hooks`) | `~/.bob/settings/settings.json` | matcher groups | seconds |
| `cortex` | `.cortex/settings.json` (`hooks`) | `~/.snowflake/cortex/hooks.json` | matcher groups | seconds |
| `goose` | `.agents/plugins/ai-rulez/hooks/hooks.json` | `~/.agents/plugins/ai-rulez/hooks/hooks.json` | `hooks`, matcher groups (a hook-only plugin directory) | seconds |
| `deepagents` | `.deepagents/hooks.json` | `~/.deepagents/hooks.json` (`DEEPAGENTS_HOME`) | `hooks`, matcher groups, exec-form `argv` | seconds |
| `junie` | none: Junie ignores project hooks | `~/.junie/config.json` (`hooks`) | matcher groups | seconds |
| `zcode` | none: ZCode ignores project hooks | `~/.zcode/cli/config.json` (`hooks.events`, and `hooks.enabled`) | matcher groups, `timeoutMs` | milliseconds (converted) |
| `crush` | `crush.json` (`hooks`) | `~/.config/crush/crush.json` | one flat list of entries per event | seconds |
| `poolside` | `.poolside/settings.yaml` (`hooks`) | `~/.config/poolside/settings.yaml` | YAML, one flat list per event, every entry has a `matcher` (`*` by default) | seconds |
| `reasonix` | `.reasonix/settings.json` (`hooks`) | `~/.reasonix/settings.json` | one flat list per event, `match` | milliseconds (converted) |
| `hermes` | none: no project hooks documented | `~/.hermes/config.yaml` (`hooks`, `HERMES_HOME`) | YAML, one flat list per event | seconds |
| `kiro` | `.kiro/hooks/ai-rulez.json` | `~/.kiro/hooks/ai-rulez.json` | `version: "v1"` and one flat `hooks` list, each entry has its `trigger` and an `action` | seconds |
| `vibe` | `.vibe/hooks.toml` | `~/.vibe/hooks.toml` (`VIBE_HOME`) | TOML, one flat `[[hooks]]` list, the event in `type`, the tool glob in `match` | seconds |
| `kimi` | none: no project hooks documented | `~/.kimi-code/config.toml` (`[[hooks]]`, `KIMI_CODE_HOME`) | TOML, one flat `[[hooks]]` list, the event in `event` | seconds |
| `cline` | `.clinerules/hooks/` | `~/Documents/Cline/Hooks/` | one executable script per event | n/a |

Supported events per harness, by Claude Code name; a native name in parentheses is shown where it differs.
An event not listed is skipped for that harness with a warning, and so is a matcher on an event the vendor
says ignores one:

| Harness | Events |
| ------- | ------ |
| `copilot-cli` | `SessionStart` (`sessionStart`), `UserPromptSubmit` (`userPromptSubmitted`), `PreToolUse` (`preToolUse`), `PermissionRequest` (`permissionRequest`), `PostToolUse` (`postToolUse`), `PostToolUseFailure` (`postToolUseFailure`), `Notification` (`notification`), `SubagentStart` (`subagentStart`), `SubagentStop` (`subagentStop`), `Stop` (`agentStop`), `PreCompact` (`preCompact`), `SessionEnd` (`sessionEnd`) |
| `factory` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Notification`, `SubagentStop`, `Stop`, `PreCompact`, `SessionEnd` |
| `antigravity` | `PreToolUse`, `PostToolUse`, `Stop` |
| `qwen` | `SessionStart`, `UserPromptSubmit`, `UserPromptExpansion`, `PreToolUse`, `PermissionRequest`, `PermissionDenied`, `PostToolUse`, `PostToolUseFailure`, `PostToolBatch`, `Notification`, `MessageDisplay`, `SubagentStart`, `SubagentStop`, `Stop`, `StopFailure`, `InstructionsLoaded`, `PreCompact`, `PostCompact`, `SessionEnd` |
| `augment` | `SessionStart`, `PreToolUse`, `PostToolUse`, `Stop`, `SessionEnd` |
| `codebuddy` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PermissionDenied`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `SubagentStart`, `SubagentStop`, `TaskCreated`, `TaskCompleted`, `Stop`, `StopFailure`, `InstructionsLoaded`, `ConfigChange`, `CwdChanged`, `FileChanged`, `WorktreeCreate`, `WorktreeRemove`, `PreCompact`, `PostCompact`, `Elicitation`, `ElicitationResult`, `SessionEnd` |
| `qoder` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PermissionDenied`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `SubagentStart`, `SubagentStop`, `Stop`, `StopFailure`, `InstructionsLoaded`, `ConfigChange`, `CwdChanged`, `FileChanged`, `WorktreeCreate`, `WorktreeRemove`, `PreCompact`, `PostCompact`, `Elicitation`, `ElicitationResult`, `SessionEnd` |
| `commandcode` | `SessionStart`, `PreToolUse`, `PostToolUse`, `Stop` |
| `letta` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `SubagentStop`, `Stop`, `PreCompact`, `SessionEnd` |
| `gitlab-duo` | `SessionStart` |
| `devin` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `Stop`, `PostCompact` (`PostCompaction`), `SessionEnd` |
| `grok` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionDenied`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `SubagentStart`, `SubagentStop`, `Stop`, `StopFailure`, `PreCompact`, `PostCompact`, `SessionEnd` |
| `bob` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop` |
| `cortex` | `SessionStart`, `Setup`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `Notification`, `SubagentStop`, `Stop`, `PreCompact`, `SessionEnd` |
| `goose` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `Stop`, `SessionEnd` |
| `deepagents` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `Notification`, `SubagentStart`, `SubagentStop`, `Stop`, `PreCompact`, `SessionEnd` |
| `junie` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `Stop`, `StopFailure`, `SessionEnd` |
| `zcode` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PostToolUseFailure`, `Stop` |
| `crush` | `PreToolUse` |
| `poolside` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `PreCompact` |
| `reasonix` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `SubagentStop`, `Stop`, `StopFailure`, `PreCompact`, `SessionEnd` |
| `hermes` | `SessionStart` (`on_session_start`), `PreToolUse` (`pre_tool_call`), `PostToolUse` (`post_tool_call`), `SubagentStop` (`subagent_stop`), `SessionEnd` (`on_session_end`) |
| `kiro` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop` |
| `vibe` | `PreToolUse` (`pre_tool`), `PostToolUse` (`post_tool`), `Stop` (`post_agent`) |
| `kimi` | `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `SubagentStart`, `SubagentStop`, `Stop`, `StopFailure`, `PreCompact`, `PostCompact`, `SessionEnd` |
| `cline` | `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `SessionStart` (`TaskStart`), `Stop` (`TaskComplete`) |

A test compares this table with the renderer's event tables. Mappings that are an inference, not something the
vendor documents: `Stop` to Vibe's `post_agent`, to Cline's `TaskComplete` and to Copilot CLI's `agentStop`;
`SessionStart` to Cline's `TaskStart`. Review them when you target those harnesses.

Matchers follow what each vendor documents. Claude Code names (`Bash`, `Edit|Write`) are copied to the
harnesses that accept them: `claude`, `codex`, `qwen`, `codebuddy`, `qoder`, `letta`, `grok`, `deepagents`,
`junie`, `zcode`. For the harnesses in the next section the matcher is rewritten; for every other one
(`commandcode`, `bob`, `reasonix`, `hermes`, `kimi`, ...) a group with a `matcher` needs `matchers.`.
Two matchers are not tool names at all: Duo CLI's matches the session source (`startup` or `resume`) and Vibe's
`match` is a glob (or a regex behind `re:`). Where a vendor requires a matcher field Poolside and Vibe get `*`
when the group sets none.

##### Tool names in matchers

A matcher is rewritten token by token over its top-level alternation (`Edit|Write`, `^Bash$`,
`mcp__github__.*`) through the vocabulary in `internal/toolnames`, the one table that hook matchers, the
plugin runtimes (`opencode`, `kilo`, `pi`, `amp`) and this page share. A Claude name outside the harness's
vocabulary, a regular expression more complex than a name, or an alternation in a harness whose matcher is a
glob or an exact name skips the whole group with a warning naming the token; a translation is never partial.
Harnesses that test the matcher as an unanchored regular expression get each name anchored (`^Execute$`), since a
Claude matcher is a whole-name match. Names below were read from the cited pages on 2026-10-05; a tool a vendor
does not document is left out.

| Harness | Claude tool to native name | MCP tools | Source |
| --- | --- | --- | --- |
| `cursor` | Bash `Shell`; Read `Read`; Edit, MultiEdit, Write `Write` (Cursor files edits under Write); Grep `Grep`; Task, Agent `Task` | not rewritten (`MCP:` carries no server) | cursor.com/docs/hooks |
| `gemini` | Bash `run_shell_command`; Read `read_file`, `read_many_files`; Edit, MultiEdit `replace`; Write `write_file`; Grep `grep_search`; Glob `glob`; LS `list_directory`; WebFetch `web_fetch`; WebSearch `google_web_search`; TodoWrite `write_todos` | `mcp__` | geminicli.com/docs/reference/tools/ |
| `copilot`, `copilot-cli` | Bash `bash`; Read `view`; Edit, MultiEdit `edit`; Write `create`; Grep `grep`; Glob `glob`; WebFetch `web_fetch`; Task, Agent `task` | not documented | docs.github.com/en/copilot/reference/hooks-configuration |
| `factory` | Bash `Execute`; Read `Read`; Edit `Edit`; Write `Create`; Grep `Grep`; Glob `Glob`; LS `LS`; WebFetch `FetchUrl`; WebSearch `WebSearch`; Task, Agent `Task` | `mcp____` | docs.factory.com/reference/hooks-reference |
| `devin` | Bash `exec`; Read `read`; Edit `edit`; Write `write`; Grep `grep`; Glob `glob`; WebFetch `webfetch` | `mcp____` | docs.devin.ai/cli/extensibility/hooks/lifecycle-hooks |
| `kiro` | Bash `shell` (one tool, no alternation; Kiro's other categories span several Claude tools) | not rewritten | kiro.dev/docs/hooks/types/ |
| `goose` | Bash `shell`; Write `write`; Edit, MultiEdit `edit` | `__` | goose-docs.ai/docs/guides/context-engineering/hooks/ |
| `crush` | Bash `bash`; Read `view`; Edit `edit`; Write `write`; MultiEdit `multiedit`; Grep `grep`; Glob `glob`; LS `ls`; Task, Agent `agent` | `mcp__` | github.com/charmbracelet/crush docs/hooks/README.md |
| `cortex` | Bash `bash`; Read `read`; Edit `edit`; Write `write`; Grep `grep`; Glob `glob`; NotebookEdit `notebook_edit_cell` | `mcp____` | docs.snowflake.com/en/user-guide/cortex-code/extensibility |
| `poolside` | Bash `shell`; Read `read`; Edit, MultiEdit `edit`; Write `write`; WebFetch `web_fetch`; WebSearch `web_search` | not documented | docs.poolside.ai/hooks |
| `vibe` | Bash `bash`; Grep `grep` (a glob: one tool, no alternation) | not rewritten | docs.mistral.ai/vibe/code/cli/hooks |
| `augment` | Bash `launch-process`; Read `view`; Edit, MultiEdit `str-replace-editor`; Write `save-file`; WebFetch `web-fetch`; WebSearch `web-search` | not rewritten (`mcp:` prefix, `_`) | docs.augmentcode.com/cli/hooks |
| `antigravity` | Bash `run_command`; Read `view_file`; Edit `replace_file_content`; MultiEdit `multi_replace_file_content`; Write `write_to_file`; Grep `grep_search`; Glob `find_by_name`; LS `list_dir`; WebFetch `read_url_content`; WebSearch `search_web`; Task, Agent `invoke_subagent` | not documented | antigravity.google/docs/hooks |

Left unmapped on purpose: Command Code (the page names the tools in two cases, `SHELL` and `shell`), Bob (only
`write_file` is documented) and Reasonix (no `match` documentation could be read). Cursor's `beforeShellExecution`
is not used for `PreToolUse` with `Bash`: its matcher is tested against the command text, not the tool, so it
is not equivalent; the `preToolUse` matcher `Shell` is. Copilot hooks carry the tool name on stdin as well, but
no sh filter is generated: the documented `matcher` makes one unnecessary.

Handlers are `command` handlers everywhere; a handler of another `type`, and a handler field the harness lacks
(`if`, `async`), is skipped with a warning like for the harnesses above. `script` is addressed through the
variable the vendor documents:

| Harness | Command for `script = "scripts/guard.sh"` |
| ------- | ----------------------------------------- |
| `qwen` | `"$QWEN_PROJECT_DIR"/scripts/guard.sh` |
| `codebuddy` | `"$CODEBUDDY_PROJECT_DIR"/scripts/guard.sh` |
| `qoder` | `"$QODER_PROJECT_DIR"/scripts/guard.sh` |
| `commandcode` | `"$COMMANDCODE_PROJECT_DIR"/scripts/guard.sh` |
| `factory` | `"$FACTORY_PROJECT_DIR"/scripts/guard.sh` |
| `devin` | `"$DEVIN_PROJECT_DIR"/scripts/guard.sh` |
| `cortex` | `"$CORTEX_PROJECT_DIR"/scripts/guard.sh` |
| `gitlab-duo` | `"$DUO_PROJECT_DIR"/scripts/guard.sh` |
| `grok` | `"$GROK_WORKSPACE_ROOT"/scripts/guard.sh` |
| `deepagents` | `"${CLAUDE_PROJECT_DIR}"/scripts/guard.sh` |
| `letta`, `antigravity`, `bob`, `kiro`, `vibe` | `./scripts/guard.sh` (the vendor's examples run project scripts relative to the project) |
| `cline` | `"$root"/scripts/guard.sh`, where the wrapper computes `$root` from its own location |
| `crush` | `"$CRUSH_PROJECT_DIR"/scripts/guard.sh` |
| `augment`, `goose`, `poolside`, `reasonix` | skipped with a warning in project scope (no documented way to address the project); use `command` |

In user scope every `script` is the absolute path in the user config directory.

Not generated, and why:

- `junie`, `zcode`, `hermes` and `kimi` have no project-level hooks (Junie and ZCode ignore them explicitly), so a
  project `generate` skips them with a warning and `generate --user` writes them.
- `codewhale`: its project hooks file loads only after the user approves a digest of its exact bytes, and
  its events do not map to Claude Code's without guessing.
- `continue-dev`: the preset no longer exists, and its hook format is documented only in source code.
- Copilot CLI's `preMcpToolCall` and Augment's `PromptSubmit` appear in third-party mappings but in no vendor
  documentation, so they are not emitted.

Some harnesses need a step from you before a generated project hook runs, which generation cannot do: Duo CLI
needs `--enable-project-hooks` (or `GITLAB_ENABLE_PROJECT_HOOKS=true`), Grok needs `/hooks-trust`, Deep Agents
asks to trust the file. Devin also loads the hooks of `.claude/settings.json`, so a
hook generated for both `claude` and `devin` runs twice. Each of these is printed once per run.

###### Cline

Cline has no hooks document: it runs the executable named after an event from `.clinerules/hooks/` (and from
`~/Documents/Cline/Hooks/`). `generate` writes one executable POSIX shell script per event
(`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `TaskStart`, `TaskComplete`). Each runs the
configured commands in order with the hook input on stdin (read up to 16 MiB); each command runs inside a
`{ ...; }` group, so `cd dir && ./check` or a trailing `# comment` behave as written and its exit status is
the group's. The last command's output is the hook's output and the first failing command stops the rest. Cline has no matcher, timeout or `async`, so a group or handler that
sets one is skipped with a warning. The scripts are owned whole, gitignored when `gitignore = true`, and removed by `clean`; a script
is treated as generated only when its second line (after the shebang) starts with `# Generated by ai-rulez`,
so a script you wrote under the same name, even one that mentions the marker in a comment, is never
overwritten (a warning says so). Cline on Windows expects `.ps1` files,
which are not generated. The vendor's current documentation describes only SDK plugins; the file contract was
read from Cline's source (see Vendor sources).

##### Plugin-based harnesses

OpenCode, Kilo, MiMo Code, Pi and Amp have no hooks file: their hooks are code, loaded from a plugin or
extension directory. `[[hooks]]` render for them (`targets` and `matchers` accept `opencode`, `kilo`,
`mimocode`, `pi` and `amp`) into one JavaScript or TypeScript module that ai-rulez owns whole. With `gitignore = true` it is
gitignored (the module only, never its directory, which also holds hand-written plugins), removed by
`clean` and by dropping the last hook, and written to the user-level directory by `generate --user`.

| Harness | Project file | User file (`--user`) | Plugin API |
| ------- | ------------ | -------------------- | ---------- |
| `opencode` | `.opencode/plugins/ai-rulez-hooks.js` | `~/.config/opencode/plugins/ai-rulez-hooks.js` | default export with the v2 `setup(ctx)` and the v1 `server(ctx)`, so it loads on either generation |
| `kilo` | `.kilo/plugins/ai-rulez-hooks.js` | `~/.config/kilo/plugins/ai-rulez-hooks.js` | default export `{ id, server }` |
| `mimocode` | `.mimocode/plugins/ai-rulez-hooks.js` | `~/.config/mimocode/plugins/ai-rulez-hooks.js` | default export `{ id, server }` |
| `pi` | `.pi/extensions/ai-rulez-hooks.ts` | `~/.pi/agent/extensions/ai-rulez-hooks.ts` | default-exported factory, `pi.on(event, handler)` |
| `amp` | `.amp/plugins/ai-rulez-hooks.ts` | `~/.config/amp/plugins/ai-rulez-hooks.ts` | default-exported function, `amp.on(event, handler)` |

Events (Claude Code name, then the native event):

| Claude Code event | opencode, kilo, mimocode | pi | amp |
| ----------------- | ------------------------ | -- | --- |
| `SessionStart` | `session.created` | `session_start` | `session.start` |
| `SessionEnd` | - | `session_shutdown` | - |
| `UserPromptSubmit` | - | `input` (can cancel the prompt) | `agent.start` |
| `PreToolUse` | `tool.execute.before` (can block) | `tool_call` (can block) | `tool.call` (can block) |
| `PostToolUse` | `tool.execute.after` | `tool_result`, when the call succeeded | `tool.result`, status `done` |
| `PostToolUseFailure` | - | `tool_result`, when `isError` | `tool.result`, status `error` |
| `PreCompact` / `PostCompact` | - / `session.compacted` | `session_before_compact` / `session_compact` | - |
| `Stop` | `session.idle` | `agent_settled` | `agent.end` |

An event outside the column is skipped with a warning. OpenCode reports `session.created` and
`session.idle` for subagent sessions too, so those hooks run for them as well; Pi's `agent_settled`
fires once when a run is final.

What the generated module does for every command:

- The command runs in a shell from the project directory with a Claude Code style JSON document on stdin:
  `session_id`, `cwd`, `hook_event_name` and the fields of the event (`tool_name`, `tool_input`,
  `tool_response`, `prompt`, `source`, `trigger`, `reason`). The harness defines no hook payload of its
  own. `tool_name` is the Claude Code name where one is mapped (`bash` becomes `Bash`, `edit_file`
  becomes `Edit`) and `harness_tool_name` is the harness's own name; `tool_input` also carries `command`
  and `file_path` as Claude Code names them. `CLAUDE_PROJECT_DIR` and `AI_RULEZ_PROJECT_DIR` hold the
  project directory.
- A `matcher` is a regular expression that must match a whole tool name, and it is tested against the
  harness's name and the Claude Code names mapped to it, so `Edit|Write` selects OpenCode's `edit`,
  `write` and `apply_patch` and Pi's `edit` and `write`. A name outside the table (an MCP tool) matches only
  by its own name, so write it in `matchers.`. On `SessionStart` the matcher is tested against the
  source (`startup`, and for Pi `resume` and `clear`), on `PreCompact` and `PostCompact` against `manual`
  or `auto` (Pi). On any other event without a subject a matcher skips the group with a warning.
- Exit code 2 from a `PreToolUse` hook blocks the call and its stderr becomes the reason (Pi also blocks a
  prompt on `UserPromptSubmit`); so does a JSON decision of `block` or `deny` on stdout. Any other non-zero
  exit, a timeout (`timeout`, default 600 seconds) or a spawn failure is logged to stderr and never blocks.
  Hooks of other events cannot block. `async = true` runs the command without waiting for it.
- A handler with `if` or a non-command `type` is skipped with a warning, as for the other harnesses.
- A `script` runs as `./