# agnostic-ai: full documentation Generated from docs/site/content/docs/ at 29b0f5012b9c0783095adbc6e19530ed8f1605e5. Source of truth: https://github.com/Chemaclass/agnostic-ai --- New to agnostic-ai? Read [Why agnostic-ai?](https://agnostic-ai.org/docs/why-agnostic-ai/), then [Set up your project](https://agnostic-ai.org/docs/getting-started/). Already set up? Pick a guide, or look up a command or field in Reference. ## Find something else | You want to | Go to | |---|---| | Work in a project that is already set up | Install, run `agnostic-ai sync`, then follow [Daily use](https://agnostic-ai.org/docs/getting-started/#daily-use) | | See your configuration and warnings in a browser | [Inspect your configuration](https://agnostic-ai.org/docs/inspect-configuration/) | | Look up a command, field, or supported tool | [CLI reference](https://agnostic-ai.org/docs/cli-reference/), [Configuration](https://agnostic-ai.org/docs/configuration/), [Supported tools](https://agnostic-ai.org/docs/targets/) | | Fix a failed command or missing file | [Troubleshooting](https://agnostic-ai.org/docs/troubleshooting/) | ## Terms used in these guides A **spec** is a source file you edit. A **target** is a coding tool. **Generated files** are what sync writes for each tool, and **drift** means they no longer match their sources. For coding agents, [agent-setup.txt](https://agnostic-ai.org/agent-setup.txt) holds the setup checklist. [llms.txt](https://agnostic-ai.org/llms.txt) lists the guides, and [llms-full.txt](https://agnostic-ai.org/llms-full.txt) holds the full documentation. Contributing to agnostic-ai itself? Use the [contributor setup](https://github.com/Chemaclass/agnostic-ai/blob/main/CONTRIBUTING.md). --- # Installation Install the `agnostic-ai` command on macOS, Linux, or Windows. It does not install Claude Code, Codex, or any other coding tool. npm needs Node 18 or newer. The other methods install a ready-made binary that needs neither Node nor Go. On macOS, you need 13 Ventura or later. Install options and commands: https://agnostic-ai.org/docs/installation/ The browser interface (`agnostic-ai ui`) ships inside the binary, so it needs no extra download. See [the view reference](https://agnostic-ai.org/docs/cli-reference/inspect/#view). ## Verify the install ```bash agnostic-ai --version # agnostic-ai version X.Y.Z ``` If the command is not found, open a new terminal or add the install directory to `PATH`. To prove the binary came from this repository, see [Verify a release](https://agnostic-ai.org/docs/verify-a-release/). Next, [set up your project](https://agnostic-ai.org/docs/getting-started/). If it already has `CLAUDE.md`, `AGENTS.md`, or other tool files, also read [Import an existing setup](https://agnostic-ai.org/docs/migration/). ### Upgrade agnostic-ai or pin a project version ## Upgrade Run `agnostic-ai upgrade`. Package-manager installs upgrade through their package manager. See [upgrade and migrate](https://agnostic-ai.org/docs/cli-reference/upgrades/#upgrade) for every option. ## Pin a version or directory The install script reads two environment variables: ```bash curl -fsSL https://raw.githubusercontent.com/Chemaclass/agnostic-ai/main/scripts/install.sh \ | AGNOSTIC_AI_VERSION=vX.Y.Z AGNOSTIC_AI_INSTALL_DIR="$HOME/bin" bash ``` On Windows, download [`install.ps1`](https://github.com/Chemaclass/agnostic-ai/blob/main/scripts/install.ps1) and pass its options: ```powershell .\install.ps1 -Version vX.Y.Z -InstallDir C:\tools\agnostic-ai ``` With npm: `npm install -g agnostic-ai@X.Y.Z`. ## Pin it per project A JavaScript project can add the CLI as a dev dependency. Then every clone and CI job runs the release in the lockfile: ```bash pnpm add -D agnostic-ai@X.Y.Z # or npm install -D, yarn add -D, bun add -D ``` Then run `pnpm exec agnostic-ai upgrade --requires` (or your manager's equivalent). It raises `requires` to that release and keeps its form. Commit the config and dependency changes together. After a pull bumps both, run your package manager's install. See [`upgrade --requires`](https://agnostic-ai.org/docs/cli-reference/upgrades/#upgrade-options) and [Node monorepos](https://agnostic-ai.org/docs/git-hooks/#node-monorepos) for syncing on install. ## Optional extras - Shell completion for Bash, Zsh, Fish, and PowerShell: [completion](https://agnostic-ai.org/docs/cli-reference/maintain/#completion). - The Claude Code [plugin](https://github.com/Chemaclass/agnostic-ai/tree/main/plugins/agnostic-ai) adds install, setup, import, and sync commands: ```text /plugin marketplace add Chemaclass/agnostic-ai /plugin install agnostic-ai@chemaclass ``` ### Install with Go or download an archive ## Other install methods | Method | Instructions | |---|---| | Go | `go install github.com/chemaclass/agnostic-ai/cmd/agnostic-ai@latest`, with the Go version from [go.mod](https://github.com/Chemaclass/agnostic-ai/blob/main/go.mod) or newer. Put `$(go env GOPATH)/bin` on `PATH`. | | Manual | Download your archive and `checksums.txt` from [GitHub Releases](https://github.com/Chemaclass/agnostic-ai/releases), verify the checksum, and put the binary on `PATH`. | ## Troubleshooting ### npm reports a missing platform package npm skipped the optional dependency that holds the binary. This happens with `--omit=optional`, and with a lockfile copied from another platform. Reinstall with the optional dependencies included: ```bash npm install -g agnostic-ai --force --include=optional ``` Drop `-g` for a project install. `--include=optional` also overrides an `omit=optional` left in your npm config. The CLI's error message names the right form for your install. ### How the npm package works [`agnostic-ai`](https://www.npmjs.com/package/agnostic-ai) is the only package you install. It's a wrapper, and its optional dependencies are six platform packages in the [`@agnostic-ai`](https://www.npmjs.com/org/agnostic-ai) organization. npm installs only the one matching your OS and CPU, such as [`@agnostic-ai/darwin-arm64`](https://www.npmjs.com/package/@agnostic-ai/darwin-arm64). npm obtains the wrapper and matching platform package from its registry or cache. No install script makes a separate binary download, so `--ignore-scripts` does not block installation. Offline installs need both packages in the cache or mirror. Set `AGNOSTIC_AI_BINARY` to an absolute path to run a binary the package does not ship. --- # Set up agnostic-ai with a coding agent Setup prompt and checklist: https://agnostic-ai.org/agent-setup.txt Open your coding agent in the project folder. It asks which tools the project uses and whether Git should track the generated files. Setup is done when `agnostic-ai sync --check` passes. Then [try a first session](https://agnostic-ai.org/docs/getting-started/#sync) and learn [the daily loop](https://agnostic-ai.org/docs/getting-started/#daily-use). To set up by hand instead, follow the [manual walkthrough](https://agnostic-ai.org/docs/getting-started/). ### Read the full checklist your coding agent follows ## What the agent does Use this checklist when a user asks you to install or set up agnostic-ai in a repository. Preserve the existing tool setup, store source instructions in `.agnostic-ai/`, and finish with a passing `sync --check`. ### Setup rules {#safety-contract} - Work from the repository root. Read its contributor and agent instructions first. - Run `git status --short`. Never discard user changes. If the tree is not clean, tell the user before writing so they can tell your changes from theirs. - Look for existing instructions files such as `CLAUDE.md`, `AGENTS.md`, and `GEMINI.md`. Tell the user what they hold and ask whether to import them before any sync. - Import existing tool files before the first sync. Sync writes files; it does not import them. - Enable only the targets the project uses or the user names. Never enable every target. - Preview before writing. If sync reports a file it does not own or would overwrite, stop and name the path. - Never print token or key values found in imported settings or MCP config. - Stop and ask when a step fails or the project state does not match this checklist. - After setup, edit instructions and specs under `.agnostic-ai/`, and project settings in `agnostic-ai.yaml`. The tool files are generated. ### 1. Install or verify the CLI ```bash agnostic-ai --version ``` If missing, ask the user before installing. The script downloads the latest release and checks it against the published checksums: ```bash # macOS or Linux curl -fsSL https://raw.githubusercontent.com/Chemaclass/agnostic-ai/main/scripts/install.sh | bash ``` ```powershell # Windows PowerShell irm https://raw.githubusercontent.com/Chemaclass/agnostic-ai/main/scripts/install.ps1 | iex ``` Run `--version` again. If the command is not found, add the install directory to `PATH`. [Installation](https://agnostic-ai.org/docs/installation/) covers other methods. ### 2. Detect the project state | State | Signal | Action | |---|---|---| | Already set up | `agnostic-ai.yaml` and `.agnostic-ai/` exist | Keep its selected tools and Git policy. Skip `init`. Go to step 4. | | Existing tool files | `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursor/`, `.github/copilot-instructions.md` | Import during `init`. | | Fresh | Neither | Plain `init`. | If the repository has only one of `agnostic-ai.yaml` or `.agnostic-ai/`, inspect it before initializing; do not replace a partial setup. If you are unsure which tools to enable, ask the user. A tool installed on the machine may not be one the repository uses. ### 3. Initialize Ask the user whether Git should track the generated files. Pass `--gitignore=on` to ignore them (teammates run `sync` after cloning) or `--gitignore=off` to commit them (CI runs `sync --check`). Replace `claude,codex` with the project's tools: ```bash # macOS or Linux, existing tool files printf '%s\n' 'claude,codex' | agnostic-ai init --from all --gitignore=on # macOS or Linux, fresh project printf '%s\n' 'claude,codex' | agnostic-ai init --gitignore=on ``` ```powershell # Windows PowerShell, existing tool files 'claude,codex' | agnostic-ai init --from all --gitignore=on # Windows PowerShell, fresh project 'claude,codex' | agnostic-ai init --gitignore=on ``` `--from all` adds each source's missing sections to `.agnostic-ai/AGNOSTIC_AI.md` and names them. Check that file for duplicates before you sync. Import tells you when a section needs a manual merge. See [Migration](https://agnostic-ai.org/docs/migration/). Use `--demo` only if the user asks. Add rules only for conventions the repository or user already states. ### 4. Validate and preview ```bash agnostic-ai validate agnostic-ai lint agnostic-ai sync --dry-run ``` Check that the targets, output paths, and kept instructions match the project. Fix errors in the specs. Read warnings about instructions or fields a tool cannot use. A passing sync check means files match their sources; it does not mean every tool can enforce every rule. See [Tool behavior](https://agnostic-ai.org/docs/target-behavior/) before changing restrictions or hiding warnings. ### 5. Sync and prove the result ```bash agnostic-ai sync agnostic-ai sync --check git status --short git diff -- . ``` Check the generated files and `.gitignore`. Inspect ignored output files directly: `git diff` does not show them. If Git tracks the generated files, include them with their specs when committing. Report: - agnostic-ai version and install method; - targets and imported sources; - what each tool now reads from `.agnostic-ai/`, as the first `agnostic-ai sync` lists it; - files changed under `.agnostic-ai/`; - the generated files and whether Git tracks them; - results of `validate`, `lint`, and `sync --check`; - decisions left for the user. Setup is done only when `agnostic-ai sync --check` exits 0. --- # Set up your project {#getting-started} This walkthrough writes one project note, generates the files Claude Code and Codex read, then changes the note and updates both. It takes a few minutes. Run each step from your project folder, with the tools you use. Prefer to have your coding agent do the setup? [Paste this prompt](https://agnostic-ai.org/docs/agent-setup/) instead, then come back for [the daily loop](#daily-use). ## Install agnostic-ai Install options and commands: https://agnostic-ai.org/docs/installation/ Check that the command works: ```bash agnostic-ai --version ``` If the command is not found, open a new terminal or see [Installation](https://agnostic-ai.org/docs/installation/#verify-the-install). ## Initialize your project {#quickstart} ```bash agnostic-ai init ``` Select `claude` and `codex` for this example, or the tools you use. These are the names agnostic-ai uses for Claude Code and Codex. `init` creates `agnostic-ai.yaml`, the project note `.agnostic-ai/AGNOSTIC_AI.md`, and a block in `.gitignore`. When asked about Git, keep **Ignore them** (the default). You commit the sources, and each teammate runs `sync` after cloning. See [Commit or ignore generated outputs](#commit-or-ignore-generated-outputs) for the other choice. **Already have tool files?** In a terminal, `init` offers to import existing files such as `CLAUDE.md` and `AGENTS.md`. Accept to keep their instructions, and check the imported sources before you sync. Import tells you when a section needs a manual merge. To import only some files, see the [import reference](https://agnostic-ai.org/docs/migration/#import-existing-tool-configuration). If `agnostic-ai.yaml` and `.agnostic-ai/` already exist, skip `init`. ## Write a project note {#scaffold} Open `.agnostic-ai/AGNOSTIC_AI.md` in your editor. Replace the placeholder comment with one note, or add it below any imported instructions, for example: ```markdown Run the project tests before finishing a change. ``` Use your project's real test command if you know it. This file is the only source you edit in this walkthrough. ## Generate and inspect {#sync} Preview the files first: ```bash agnostic-ai sync --plan ``` Sync can replace generated files. If the plan names a file it does not own or would overwrite, stop and check that its instructions were imported. Then generate and check: ```bash agnostic-ai sync agnostic-ai sync --check ``` `sync --check` exits 0 when the generated files match their sources. In this Claude Code and Codex example, you get: | Tool | Generated project notes | |---|---| | Claude Code | `CLAUDE.md`, which points to `AGENTS.md` | | Codex | `AGENTS.md` | Find "Run the project tests before finishing a change." in `AGENTS.md`. `CLAUDE.md` contains `@AGENTS.md`, so Claude Code reads the same note. With only Claude Code selected, the note goes straight into `CLAUDE.md`, and an imported setup can keep its full Claude text there. For other tools, open their generated files and follow any references. To see it work, start a new Claude Code or Codex session in this folder and ask: "How should you check a change before finishing?" Matching files show the note was delivered, not that the model followed it; see each [tool's loading limits](https://agnostic-ai.org/docs/targets/). ## Change one note, update both tools {#daily-use} Add a sentence to `.agnostic-ai/AGNOSTIC_AI.md`: ```markdown Summarize which tests you ran. ``` Run: ```bash agnostic-ai sync --plan agnostic-ai sync agnostic-ai sync --check ``` Open `AGENTS.md` to find the new sentence. `CLAUDE.md` still points to it, so both tools read the update. This is the daily loop: edit the source, review the plan, sync. Never edit the generated files; sync replaces them. After cloning, install agnostic-ai the way the project documents, then run `sync`. If the project lists agnostic-ai as a dependency, install its locked dependencies and use the local command, such as `pnpm exec agnostic-ai`. ### Watch for edits, inspect status, or add another coding tool Use `agnostic-ai sync --watch` while editing. `agnostic-ai status` lists loaded specs, selected tools, and out-of-date files. Run `agnostic-ai use codex` to add Codex to an existing setup, or change `targets:` in [configuration](https://agnostic-ai.org/docs/configuration/#targets). [Checkout hooks](https://agnostic-ai.org/docs/git-hooks/#regenerate-on-checkout) restore ignored files in new checkouts. Automated environments can use [project readiness checks](https://agnostic-ai.org/docs/cli-reference/project/#project-readiness). ## Commit or ignore generated outputs Choose this during `init`: - **Ignore (default):** commit `.agnostic-ai/`, `agnostic-ai.yaml`, `.gitignore`, and any other file sync lists to commit. Teammates install agnostic-ai and run `sync` after cloning. Use the [ignored-output CI recipe](https://agnostic-ai.org/docs/ci/#ignored-outputs). - **Commit (`init --gitignore=off`):** also commit the generated files with their source changes, so a clone has them ready. Use the [committed-output CI check](https://agnostic-ai.org/docs/ci/#committed-outputs). `git diff` does not show ignored files, so open them directly to review them. To change this choice later, see [Git ignore settings](https://agnostic-ai.org/docs/configuration/#gitignore). ## Everyday commands Run these from the folder that contains `agnostic-ai.yaml`: | Command | What it does | |---|---| | `agnostic-ai status` | Lists selected tools and out-of-date files. | | `agnostic-ai sync --plan` | Previews what sync will change. | | `agnostic-ai sync` | Writes the tool files. | | `agnostic-ai sync --check` | Confirms the tool files match their sources. | | `agnostic-ai ui` | Opens the project or user configuration in your browser. See the [browser walkthrough](https://agnostic-ai.org/docs/inspect-configuration/). | | `agnostic-ai view --json` | Prints the same configuration overview as JSON. | | `agnostic-ai doctor` | Diagnoses problems and suggests a next step. `doctor --fix` repairs generated files. | | `agnostic-ai why AGENTS.md` | Traces a generated file back to its source. | | `agnostic-ai explain --layers` | Shows which built-in, pack, project, and personal sources replace or extend a spec. | | `agnostic-ai verify --evaluation smoke-v1 --record run.json` | Saves a check you can [compare after configuration changes](https://agnostic-ai.org/docs/cli-reference/check/#record-and-compare-runs). | If something looks wrong, start with `doctor` and then [Troubleshooting](https://agnostic-ai.org/docs/troubleshooting/). The [CLI reference](https://agnostic-ai.org/docs/cli-reference/) covers every command, including import, compare, and upgrade. ### Add a standing convention when you need one ## First rule Create a rule for your team's commit convention: ```bash agnostic-ai new rule conventional-commits ``` Replace `.agnostic-ai/rules/conventional-commits.md` with: ```markdown --- name: conventional-commits description: Use the team's commit message format. alwaysApply: true --- Start commit subjects with feat:, fix:, docs:, refactor:, test:, or chore:. Keep the subject under 72 characters. ``` Use your team's prefixes. A rule asks the model to follow the convention; enforcing the format needs a Git check. Run `agnostic-ai sync --plan`, review the changes, then run `agnostic-ai sync` and `agnostic-ai sync --check`. Claude Code receives `.claude/rules/conventional-commits.md`. Codex receives a Rules section in `AGENTS.md`. ## Next steps Add more only when a task needs it: - Repeat a task with several steps: write a [skill](https://agnostic-ai.org/docs/spec-format/skills/), such as a release checklist. - Run a command at a tool event: add a [hook](https://agnostic-ai.org/docs/spec-format/hooks/). - Keep a standing convention apart from the project note: add a [rule](https://agnostic-ai.org/docs/spec-format/rules/). - Give one folder different instructions: use [directory-specific instructions](https://agnostic-ai.org/docs/scoped-context/). - Keep personal specs for every project in `~/.agnostic-ai/`: see [global configuration](https://agnostic-ai.org/docs/configuration/#global-configuration), written with `agnostic-ai sync --global`. - Using Composer? [Set up its Claude Code and Codex providers](https://agnostic-ai.org/docs/composer/) from the same sources. [Why agnostic-ai?](https://agnostic-ai.org/docs/why-agnostic-ai/#what-to-add-and-when) lists every kind of spec you can add. --- # Import existing tool configuration Import moves your current tool setup into editable sources in `.agnostic-ai/`, so you keep your current instructions. [Install agnostic-ai](https://agnostic-ai.org/docs/installation/) first, then start from the project root with a clean Git working tree so you can review and undo the import. ## Import the existing setup {#1-import} For a project not yet using agnostic-ai: ```bash agnostic-ai init --from claude ``` Use `--from all` to detect every tool, then select only the coding tools the project uses. In a project already using agnostic-ai, preview an import before writing: ```bash agnostic-ai import claude --dry-run --diff agnostic-ai import claude ``` Import writes source specs and does not sync. The `import` command does not change selected targets. ## Review what was imported {#review-what-was-imported} Compare `.agnostic-ai/AGNOSTIC_AI.md` and imported specs with the original files. With several tools, [review their import order](#2-review) before syncing. A hand-written root `AGENTS.md` contributes missing sections with `--from all`. Before committing, check scripts called by hooks and settings, and MCP connection variables. Skill folders and some tool scripts are imported; other referenced scripts must stay in your project. [MCP import details](https://agnostic-ai.org/docs/spec-format/mcp-import/) explain credential references and connection differences. ## Generate and check {#3-generate} ```bash agnostic-ai sync --dry-run --backup agnostic-ai sync --backup agnostic-ai sync --check ``` The first sync adds generated headers and replaces root instruction files with the shared instructions. Read the resulting native files before starting a new coding-tool session. ## Commit or ignore outputs {#4-commit-or-ignore-outputs} Choose a [Git strategy](https://agnostic-ai.org/docs/getting-started/#commit-or-ignore-generated-outputs). If you commit outputs, keep the first sync in its own commit. To ignore files Git already tracks, run `sync --untrack`. It runs `git rm --cached`, keeping your working copy, but other clones lose those files on the next pull. Teammates must run `agnostic-ai sync` after pulling, or install [checkout hooks](https://agnostic-ai.org/docs/git-hooks/#regenerate-on-checkout) first. From now on, edit `.agnostic-ai/` sources and sync, as [Daily use](https://agnostic-ai.org/docs/getting-started/#daily-use) shows. ## Review imports from several tools {#2-review} Preview several tools at once with `agnostic-ai import claude codex --dry-run --diff`. Imports can merge shared instructions and keep conflicting versions, so resolve differences against the original files before generating files. ### Re-import after editing a native tool file Re-running import with no edits leaves every spec as it is. After you edit a tool's own file, import brings the edit back into the spec, or stops when that would lose content. See [keep existing specs](https://agnostic-ai.org/docs/cli-reference/start/#keep-existing-specs) and [bring tool edits back](https://agnostic-ai.org/docs/cli-reference/start/#bring-tool-edits-back). [Local specs](https://agnostic-ai.org/docs/local-overrides/#import) stay out of the shared source. ### Keep directory-specific and alternate instruction files ## Keep directory-specific instructions `import codex` and `import gemini` keep nested instruction directories as `scope`. `import claude` imports `.claude/rules/` when that directory exists. Otherwise, it imports nested `CLAUDE.md` files as directory-scoped rules. If `.claude/rules/` exists alongside nested `CLAUDE.md` files, create a rule with `agnostic-ai new rule --scope ` and copy each nested file’s text into it. For directory-specific instruction files, sync replaces a hand-written file only when the specs contain all its text. Otherwise it stops and quotes the missing line. Move that line into a spec, or set the original aside: ```bash mv services/payments/AGENTS.md services/payments/AGENTS.md.before-agnostic ``` Keep the saved copy until you have checked the output. `--backup` does not bypass this directory-specific check. Root instruction files have a separate [backup and overwrite rule](https://agnostic-ai.org/docs/cli-reference/sync/#sync). ### Restore generated files from backups ## Back up and restore `sync --backup` saves a `.bak` copy before overwriting a file. To undo generated files: ```bash agnostic-ai revert ``` It restores the backups but never undoes spec edits; use Git for a lasting copy. `agnostic-ai cleanup` removes the backups. See [revert](https://agnostic-ai.org/docs/cli-reference/maintain/#revert) and [cleanup](https://agnostic-ai.org/docs/cli-reference/maintain/#cleanup). ### Upgrade an older agnostic-ai setup ## Upgrading from older versions After an upgrade, `agnostic-ai migrate --dry-run` shows which old spec forms it can rewrite, and `agnostic-ai migrate` applies them. See [migrate](https://agnostic-ai.org/docs/cli-reference/upgrades/#migrate). The changes below need more than `migrate`. ### Unmarked MCP literals **Breaking change:** `lint` and `sync` fail on an MCP `env` or `headers` value that is neither a `${NAME}` reference nor marked `!literal` (LINT035). Run `agnostic-ai migrate --only secrets`, then fix the values it leaves to you. See [secrets and plain settings](https://agnostic-ai.org/docs/spec-format/mcps/#plain-settings). ### Scope and pattern unions **Breaking change:** `scope` plus `globs` or `paths` now applies to both, not their overlap. Remove `scope` when a rule should match only its patterns, and remove `globs: "**/*"` when it should match only its scope. Preview with `agnostic-ai render`. See [scoped context](https://agnostic-ai.org/docs/scoped-context/#narrow-a-rule-to-certain-files). An upgrade can move or skip files that older versions wrote for scoped rules. Run `sync`, then `sync --check`, to remove them. ### Codex skills moved in 0.75 Codex skills moved from `.codex/skills/` to `.agents/skills/`. Add `.agents/skills/**` to `.npmignore`, `.vscodeignore`, or `.dockerignore` where you exclude them. `agnostic-ai doctor` names generated paths that a packaging ignore file misses; confirm with `npm pack --dry-run`, `vsce ls`, or your Docker context. --- # Troubleshooting Run `doctor` from the directory that holds `agnostic-ai.yaml` first: ```bash agnostic-ai doctor ``` Follow its final **Next step**. Doctor is read-only by default; [`doctor --fix`](https://agnostic-ai.org/docs/cli-reference/check/#doctor) applies its repairs, after which you rerun `doctor`. When a finding needs more detail or `doctor` cannot start, find the symptom below. | Symptom | Next step | |---|---| | `agnostic-ai` is not found | Check the [install destination and PATH](https://agnostic-ai.org/docs/installation/) | | The version did not change after upgrading | Run `agnostic-ai upgrade --check` to find an older binary that runs before the new one | | Config is missing | Change to the project root, or run `agnostic-ai init` for a new project | | A new clone or linked checkout (worktree) has no tool files | Run `agnostic-ai sync`; ignored outputs are absent from Git | | An edited spec has no effect | Run `agnostic-ai list` to confirm it loads, then `agnostic-ai sync --dry-run` to see the planned output | | Files match, but the agent ignores an instruction | Start a new session from the right directory and check the tool's [loading limits](https://agnostic-ai.org/docs/targets/) | | A tool gets only some spec kinds | Check its [capabilities](https://agnostic-ai.org/docs/targets/#capability-matrix) and any unsupported-kind warnings | | `sync --check` fails | Run `agnostic-ai sync`, review the result, and commit the generated files if the project tracks them | | CI fails on every fresh checkout | Match the [CI recipe](https://agnostic-ai.org/docs/ci/) to whether you commit generated files | | Two targets write to the same path | Read [AAI-102](https://agnostic-ai.org/docs/errors/#aai-102-targets-emit-to-the-same-output-path) and check `outputs` overrides | | `sync` refuses to write a `.*ignore` file | Read [AAI-103](https://agnostic-ai.org/docs/errors/#aai-103-hand-authored-ignore-file-would-lose-patterns), import the patterns, and check their order and negations | | A scoped rule is skipped, conflicts, or appears missing | Check [scoped-rule diagnostics](#scoped-rules) | | A generated skill links to a file the agent cannot open | Run `agnostic-ai doctor --check-references`; see [broken skill references](#broken-skill-references) | | Watch mode misses changes on a mounted filesystem | Try `agnostic-ai sync --watch --watch-poll` | | Claude Code reports that a hook timed out | Identify the [hook owner and slow command](#hook-timeouts) | ## Inspect the project After `doctor`, use the command its finding names: - [`validate`](https://agnostic-ai.org/docs/cli-reference/check/#validate) checks spec formats, [`lint`](https://agnostic-ai.org/docs/cli-reference/check/#lint) spec quality, and [`sync --check`](https://agnostic-ai.org/docs/cli-reference/sync/#sync) generated files. - [`status`](https://agnostic-ai.org/docs/cli-reference/check/#status) summarizes the project. - [why](https://agnostic-ai.org/docs/cli-reference/inspect/#why) traces a generated file to its source, and [graph](https://agnostic-ai.org/docs/cli-reference/inspect/#graph) shows which tools receive a spec. - [`explain`](https://agnostic-ai.org/docs/cli-reference/inspect/#explain) explains a diagnostic code, such as `agnostic-ai explain AAI-003` or `agnostic-ai explain LINT011`. ### Fix directory-specific rule problems ## Scoped rules | Symptom | Next step | |---|---| | `unknown flag: --scope` | Install a newer version; see [setup](https://agnostic-ai.org/docs/scoped-context/#start-with-one-directory). | | A rule is skipped or a filter cannot be kept | Check [tool support](https://agnostic-ai.org/docs/scoped-context/#native-support) and [pattern limits](https://agnostic-ai.org/docs/scoped-context/#narrow-a-rule-to-certain-files). | | Tools sharing a file conflict, or instructions differ | Use compatible tools with the same shared content, or separate worktrees. `--only` and `prefer-spec` do not bypass [scope conflicts](https://agnostic-ai.org/docs/scoped-context/#shared-files-and-safe-updates). | | A hand-written file has a line no spec holds | Move the quoted line into its spec, or set the file aside and sync. Follow [migration](https://agnostic-ai.org/docs/migration/#keep-directory-specific-instructions). | | Alternate instructions conflict | Import and preserve the alternate file, then move it off its native filename. Follow [migration](https://agnostic-ai.org/docs/migration/#keep-directory-specific-instructions). | | An `outputs` override is rejected | Remove the named override and keep headers enabled. | | Cursor has no scoped `.mdc` file | It can share a nested `AGENTS.md` with Codex. Run `agnostic-ai graph --spec ` to see. | | An old scope file remains | Run a full sync, then `sync --check`. Hand-authored files stay. | ### Fix broken references inside a skill ## Broken skill references A skill with a broken relative link still syncs. `agnostic-ai doctor --check-references` lists each broken link by source spec and destination, with every affected target on one line. A link that works from the project root, such as `apps/engine/src/lib.ts`, counts as valid even when the skill folder lacks the file. Not every tool resolves links from the project root, so prefer a path the agent can open from where it runs. A link that leaves the project never counts. | Cause | Fix | |---|---| | The linked file is in neither `.agnostic-ai/skills//` nor at that path from the project root | Add it, then run `agnostic-ai sync` | | The link leaves the skill folder, such as `../shared/setup.md`, and no file sits at that path from the project root | Move the file into the skill folder and update the link. Sync copies only the skill's own folder | | The tool flattens skills to one file and drops bundled files | Link to a URL, put the content in the skill, or accept the gap for that tool | | You deleted a generated file by hand | Run `agnostic-ai sync` to restore it | | A placeholder link in an example, such as `[Logs](url)`, can never resolve | List its destination under [`doctor.check-references.ignore`](https://agnostic-ai.org/docs/configuration/#doctorcheck-referencesignore) | ## A synced skill is disabled via skillOverrides A message such as `Skill "deploy" is disabled via skillOverrides` means your Claude Code settings switched the skill off, usually from the `/skills` menu. Sync still writes it. Run `agnostic-ai doctor` to list every synced skill your Claude settings turn off, file by file, then re-enable them in `/skills` or with `agnostic-ai doctor --fix` in a terminal. See [Claude skill overrides](https://agnostic-ai.org/docs/cli-reference/check/#claude-skill-overrides). ## Hook timeouts A message such as `UserPromptSubmit hook [...] timed out after 5s` comes from the coding tool. It does not say who installed the hook or why its command was slow. In Claude Code, open `/hooks` and find the event and command named in the message. Check project settings, user settings, and enabled plugins. A command containing `${CLAUDE_PLUGIN_ROOT}` belongs to a plugin; update or diagnose that plugin, not your agnostic-ai specs. The [Claude Code hook reference](https://code.claude.com/docs/en/hooks) explains hook locations and timeouts. For a hook generated from `.agnostic-ai/hooks/`, use [`why`](https://agnostic-ai.org/docs/cli-reference/inspect/#why) to find its source and [hook diagnostics](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/) to inspect or test it. Make changes in the source, then sync. Check whether an ordinary terminal command and the hook's runtime, such as `node --version`, are also slow. If several unrelated hooks time out together, check CPU load, memory pressure, and runtime startup first. Raise a timeout only once you know the command needs longer, because a stuck command then takes longer to report. The [Caveman built-in](https://agnostic-ai.org/docs/rtk-and-caveman/#if-you-already-use-rtk-or-caveman) supplies a skill, not the separate Caveman plugin's mode-tracking hooks. ## Report a problem Include the CLI version, OS, failing command, full error, and the smallest config or spec that shows the problem. Remove credentials from MCP configuration and logs first. Open a [bug report](https://github.com/Chemaclass/agnostic-ai/issues/new?template=bug_report.yml). --- # Inspect your configuration Use the local browser interface to find a warning, edit the source file behind it, and sync the fix. ## Open the project From the folder containing `agnostic-ai.yaml` or any folder inside it, run: ```bash agnostic-ai ui ``` Keep the terminal running. The browser opens at `http://127.0.0.1:4310`, or the next free port it prints. Outside a project, or with `--global`, it opens your user configuration. See [`view`](https://agnostic-ai.org/docs/cli-reference/inspect/#view) for ports and flags. ## Fix a warning Select a section to see its files and findings from `lint` and `doctor`. Findings appear beside the affected file or at the top of the section when no file matches. For example, a warning about bare `read` in **Authority** means the rule allows reading every file. Open the path beside the warning in your editor, or use the edit icon to open it in Cursor. If the rule should cover only `src/`, replace `read` with `read(src/**)`: ```yaml permissions: allow: - read(src/**) ``` Keep the other entries. Other rules and tool defaults still affect access; see [permission rules](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules). ## Sync and check In another terminal in the project folder: ```bash agnostic-ai sync --plan agnostic-ai sync agnostic-ai sync --check ``` Review the plan before syncing. `sync --check` should exit 0 once the generated files match the source. Then reload the browser page to see the updated findings. The view and search stay in the address bar; unsaved input is lost. For per-agent permission decisions, browser switches, and undo, see [`serve`](https://agnostic-ai.org/docs/cli-reference/inspect/#serve). Open **Settings**, then **Preferences**, in `agnostic-ai ui --global` to turn voice dictation on, choose hold or tap, and set the spoken language. Run `agnostic-ai sync --global` to apply the saved values. --- # Spec format A spec is a file under `.agnostic-ai/` that defines shared tool configuration. `agnostic-ai sync` writes it for each enabled tool in its native path and format, such as `.claude/agents/`, `.cursor/rules/`, `.codex/config.toml`, or `.gemini/settings.json`. ## Choose what to write Pick the folder from the [table below](#folders), and write the smallest spec that covers the task: when it applies, what to do, and what to return. Check it with `agnostic-ai lint` and `agnostic-ai sync --dry-run`, then sync and run `agnostic-ai sync --check`. ## Terms used in these pages - **Target:** an AI coding tool selected in `agnostic-ai.yaml`. - **Portable:** a setting written once that sync translates for several tools. - **Native:** a tool's own file format, setting, or feature. - **Frontmatter:** YAML settings between `---` lines at the start of a Markdown file. - **Scope:** the directory where a spec applies. A project-wide spec has no directory scope. - **Glob:** a file pattern such as `src/**/*.go`, rather than one exact path. - **Sync note:** a message explaining what a tool cannot use, leaves out, or handles differently. - **MCP:** Model Context Protocol, a way for an agent to use outside tools and data. A `stdio` server runs a local command; HTTP, SSE, and WebSocket (`ws`) servers connect over a network. - **Matcher:** a pattern that selects which tool calls trigger a hook. A regular expression (regex) is one pattern format. - **Entry point:** the first instruction file a tool loads, such as `AGENTS.md` or `CLAUDE.md`. ## Folders | Folder | Holds | Use it when | |--------|-------|-------------| | [`agents/`](https://agnostic-ai.org/docs/spec-format/agents/) | Subagents with their own prompt, tools, and model | a task deserves a specialist with a narrow role | | [`skills/`](https://agnostic-ai.org/docs/spec-format/skills/) | Procedures loaded on demand, with bundled files | a workflow is long, occasional, or needs scripts | | [`rules/`](https://agnostic-ai.org/docs/spec-format/rules/) | Standing conventions, for the project, a directory, or a file pattern | the agent must follow it every time | | [`commands/`](https://agnostic-ai.org/docs/spec-format/commands/) | Slash commands | a person starts the workflow by name | | [`hooks/`](https://agnostic-ai.org/docs/spec-format/hooks/) | Commands the tool runs at events such as session start | something must happen every time, not when the model remembers | | [`mcps/`](https://agnostic-ai.org/docs/spec-format/mcps/) | MCP server connections | the agent needs an outside tool or data source | | [`settings/`](https://agnostic-ai.org/docs/spec-format/settings/) | Permissions, default model, and effort | one policy should decide what agents may run | | [`reviews/`](https://agnostic-ai.org/docs/spec-format/reviews/) | Guidance for code-review bots | a review bot should check project rules | | [`environments/`](https://agnostic-ai.org/docs/spec-format/environments/) | Worktree setup, dev servers, and cleanup | agents work in fresh worktrees | | [`ignore/`](https://agnostic-ai.org/docs/spec-format/ignore/) | Context and indexing exclusions for supported tools | irrelevant files crowd search or context | | [`overlays/`](https://agnostic-ai.org/docs/spec-format/overlays/) | Native settings and helper files that `import` captured | a tool setting has no portable spec | Beside them, `AGNOSTIC_AI.md` holds the text every [entry-point file](https://agnostic-ai.org/docs/configuration/#entry-point-files) carries, and `local/` holds [personal specs](https://agnostic-ai.org/docs/local-overrides/) that stay out of Git. - Paths are relative to `.agnostic-ai/`, so `rules/*.md` means `.agnostic-ai/rules/*.md`. [`sources`](https://agnostic-ai.org/docs/configuration/#sources) overrides the directories. - Subfolders load too: every `.md` under `agents/`, `rules/`, `commands/`, `reviews/`, and `ignore/`, and every `.yaml` under `hooks/`, `mcps/`, `settings/`, and `environments/`. Under `skills/`, Markdown beside a `SKILL.md` is bundled reference material, not another skill. - `agnostic-ai new ` creates a starter spec for [each supported kind](https://agnostic-ai.org/docs/cli-reference/start/#new). [Getting started](https://agnostic-ai.org/docs/getting-started/) walks through a first rule. - The [tool support table](https://agnostic-ai.org/docs/targets/#capability-matrix) shows which tools get each kind; each tool's page shows how it writes it. ## Nested layout: per-directory scope A spec in a subdirectory of its source folder gets a **scope** equal to that subpath. ``` rules/ ├── conventional-commits.md # scope: "" (root) └── backend/ ├── auth.md # scope: "backend" └── api/limits.md # scope: "backend/api" ``` For rules, a folder scopes the rule only when it names a project directory, and a `scope:` field wins over the folder ([rule fields](https://agnostic-ai.org/docs/spec-format/rules/#fields)). A scoped rule stays out of the root instruction file: supported tools get a path condition or a nested instruction file, and other tools skip it with a warning, or fail under `on-unsupported: error`. [Directory-specific instructions](https://agnostic-ai.org/docs/scoped-context/) lists the tools and their limits. ## Target scoping Four fields limit which tools get a spec of any kind. With none set, every target that supports the kind gets it. | Field | Effect | |-------|--------| | `target` | Write only for this target. | | `targets` | Write only for these targets. | | `target-exclude` | Write for every target except this one. | | `targets-exclude` | Write for every target except these. | `target` wins over `targets`, and exclusion wins over inclusion. ### Import auto-scoping A hook imported from a tool gets `target: ` (`codex`, `claude`, or `gemini`). Delete the field to let it reach every target. `import claude` and `import codex` do the same for an agent or skill that only one of `.claude/` and `.codex/` has. A spec in both, or any spec in a single-tool project, stays shared, so an import and a sync give identical files. ## YAML headers (frontmatter) {#frontmatter-rules} - YAML between two `---` lines at the top of the file. - Empty frontmatter (`---\n---\n`) means no metadata. - A file without frontmatter still loads; its name defaults to the filename. - Malformed YAML inside a closed frontmatter block fails loading. An unclosed block becomes body text, and `lint` reports LINT006. - Fields not listed on a kind's page are written as given. ## Per-target body fences To vary text per tool, wrap the part that differs in `::target` fences. Text inside goes only to the listed tools; text outside goes to all. Marker lines start at column 0 and never reach the output. ```md Shared intro paragraph. ::target claude Claude-only section. ::end ::targets codex gemini Codex and Gemini section. ::end ``` | Syntax | Meaning | |--------|---------| | `::target ` | Opens a fence for one target. | | `::targets ` | Opens a fence for several targets. | | `::end` | Closes the last open fence. A missing `::end` runs to the end of the body. | - `import` keeps fences intact. `import codex` adds them when Claude and Codex have the same agent or skill with different bodies. - Fences also work in `.agnostic-ai/AGNOSTIC_AI.md`. A shared file is never split: a block reaches it when any tool that reads it is listed, so `::target codex` text reaches every `AGENTS.md` reader. See [Entry-point files](https://agnostic-ai.org/docs/configuration/#entry-point-files). ## Path variables: `{{$NAME}}` A spec body can name a tool's directory without hardcoding it. `{{$SKILLS_DIR}}` expands to `.claude/skills` for claude, `.agents/skills` for codex, and `.github/skills` for copilot. | Variable | Resolves to | |---|---| | `{{$SKILLS_DIR}}` | the tool's skills directory | | `{{$AGENTS_DIR}}` | the tool's agents directory | | `{{$COMMANDS_DIR}}` | the tool's commands directory | | `{{$RULES_DIR}}` | the tool's rules directory | | `{{$MCP_FILE}}` | the tool's MCP config file | - **Bodies only.** Frontmatter values do not expand. - **An `outputs..` override wins.** With `outputs.claude.skills-dir: custom/skills`, `{{$SKILLS_DIR}}` follows it. - **A variable the tool has no directory for stays as written** and raises a sync note. aider and jules resolve no variables. - **A shared file expands only when every reader agrees.** This covers the rules block of an entry point, the review section of `AGENTS.md`, and a nested `AGENTS.md`. Codex and Amp both use `.agents/skills`, so it expands; Codex and OpenCode differ, so it stays as written with one sync note naming the file. `GEMINI.md` has one reader and gets `.gemini/skills`. - **Only where the tool has its own directory.** Tools that turn agents into rules (continue, trae, windsurf) or commands (gemini) have no `{{$AGENTS_DIR}}`. Antigravity, Goose, and OpenHands resolve it to `.agents/agents`. - **The `$` is required.** Plain `{{placeholder}}` stays untouched, so Warp workflow arguments and Handlebars or Jinja text survive. Lowercase names such as `{{$skills_dir}}` do not resolve. ## Agent and skill references A spec body can name another agent or skill the way each tool invokes it. `{{$AGENT:reviewer}}` and `{{$SKILL:commit}}` become that tool's documented phrase. ```md Before pushing, run {{$AGENT:reviewer}}, then {{$SKILL:commit}}. ``` | Target | `{{$AGENT:reviewer}}` | `{{$SKILL:commit}}` | Source | |---|---|---|---| | claude | the reviewer subagent | /commit | [subagents](https://code.claude.com/docs/en/sub-agents#invoke-subagents-explicitly), [skills](https://code.claude.com/docs/en/skills#control-who-invokes-a-skill) | | codex | the reviewer agent (neutral) | $commit | [skills](https://learn.chatgpt.com/docs/build-skills.md) | | cursor | the reviewer subagent | /commit | [subagents](https://cursor.com/docs/subagents#explicit-invocation), [skills](https://cursor.com/docs/skills) | | copilot | the reviewer agent | the /commit skill | [custom agents](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli), [skills](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills) | | every other target | the reviewer agent | the commit skill | | - **Bodies only**, with the `$` and an uppercase keyword, like path variables. `{{AGENT:reviewer}}` stays untouched. - **A tool with no documented form gets a neutral phrase** and one sync note, not the raw text, which reads worse to a model. - **A shared file gets the neutral phrase**: a rule or review in the rules block of `AGENTS.md` or a nested `AGENTS.md` (so a Codex rule always does), and a skill, agent, or command in a directory two enabled tools write, such as `.agents/skills` for Codex and Windsurf. - **`lint` fails with LINT033** on an unknown name, or on a name whose spec does not sync to every target the referring spec reaches, and lists the known names. Only project specs count, since a teammate lacks your global agents. ## Target-specific extensions: `x-` namespace Use an `x-:` block for fields only one tool reads. Other tools ignore it. ```markdown --- name: code-reviewer description: Reviews diffs. model: sonnet x-claude: allowed-tools: [Read, Grep, Bash] x-cursor: globs: "src/**" alwaysApply: false --- ``` For each tool, sync removes all `x-*` keys, then moves the keys of the matching `x-` block to the top level. They replace keys with the same name. | Target | Resulting frontmatter | |--------|-----------------------| | `claude` | `name`, `description`, `model`, `allowed-tools` | | `cursor` | `name`, `description`, `model`, `globs`, `alwaysApply` | | `gemini` | `description` (`name` becomes the `.toml` filename; `model` is not written for commands) | ### Arbitrary custom keys Any other key under `x-` is written as given into that tool's output, in sorted order, and never reaches another tool. - agnostic-ai does not check them against the tool's schema. Each tool's page lists the keys it manages. - A tool with no output for a spec kind drops them. - Gemini TOML accepts only a string, bool, number, or string array, and skips nested tables. On a settings spec, the block merges into the tool's settings file by the rules in [settings](https://agnostic-ai.org/docs/spec-format/settings/#target-specific-keys). # Agents `agents/` defines subagents: specialists the main agent hands a task to. Each has its own instructions, tools, and model. Examples: a reviewer that only reads, an architect on the strongest model, a test writer on a cheaper one. Give the agent one responsibility, the files or decisions it owns, a clear return format, and how it should check its result. Write `description` as the trigger: say when to hand a task to the agent ("use proactively" makes Claude Code delegate more often). Keep detail in the body, which loads only when the agent runs; every description loads in every session. ## When not to write one - Keep work in the main conversation when it needs frequent back-and-forth, or when planning, implementation, and testing share one context. An agent starts fresh and returns a summary. - Keep a quick change, or a task where speed matters, in the main conversation. An agent may need time to gather context first. ## Write one ```bash agnostic-ai new agent code-reviewer ``` Edit `.agnostic-ai/agents/code-reviewer.md` (or its configured source path). The YAML frontmatter configures the agent; the body is its prompt. ```markdown --- name: code-reviewer description: Reviews diffs for bugs, style, and architectural issues. Use after a change set is complete. can: [read, shell(git diff *)] model: {claude: sonnet, codex: sol} --- You are a code reviewer. Report concise findings with `file:line` references. ``` Claude Code gets `tools: [Read, Bash]` and can run any command, because it ignores the pattern ([capabilities](#capabilities)). Codex drops `can` with a note ([tool support](#tools-support-by-target)). Run `agnostic-ai explain agents/code-reviewer.md` to inspect the translations, then `agnostic-ai sync` and `agnostic-ai sync --check`. ## Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | no | filename without `.md` | Agent identifier and output filename. | | `description` | no | empty; the name for Claude Code, Qoder, Antigravity, and Gemini CLI | When to hand a task to the agent. Claude Code skips an agent without one, so sync writes the name there with a note. `lint` warns when it is missing (LINT048). | | `can` | no | unset | What the agent may do, in tool-neutral names. See [capabilities](#capabilities). | | `tools` | no | unset | The same list in Claude Code tool names. Set `can` or `tools`, not both. See [support by target](#tools-support-by-target). | | `model` | no | unset | A string, a map per target, or a [tier](#model-tiers) name. See [per-target `model` and `effort`](#per-target-model-and-effort). | | `effort` | no | unset | A string or integer, or a map per target. Same section. | | `color` | no | unset | Badge color. See [`color` support by target](#color-support-by-target). | | `readonly` | no | unset | `true` limits the agent to reading; `false` does nothing. See [`readonly` by target](#readonly-by-target). | | `memory` | no | unset | Persistent memory directory scope: `user`, `project`, or `local`. [Claude Code](https://agnostic-ai.org/docs/targets/claude-reference/#agent-memory) uses it, [Qoder](https://agnostic-ai.org/docs/targets/qoder/#subagent-memory) gets the key (unconfirmed), and Junie gets it as written. Every other tool drops it. | | `mcpServers` | no | unset | MCP servers this agent may reach. See [support by target](#mcpservers-support-by-target). | | `maxTurns` | no | unset | The most turns before the agent stops, a whole number of 1 or more. See [support by target](#maxturns-and-background-by-target). | | `background` | no | unset | `true` runs the agent without making the main session wait. Same section. | | `permissionMode` | no | unset | How much this agent may do without approval. See [support by target](#agent-policy-support-by-target). | | `hooks` | no | unset | Lifecycle hooks for this agent only. Same section. | Any other frontmatter field is written as given. ### `readonly` by target {#readonly-by-target} | Target | Effect | |--------|--------| | Cursor | Restricted | | Claude Code | `disallowedTools: Write, Edit, NotebookEdit` (Bash stays allowed) | | Codex | `sandbox_mode = "read-only"`, which current Codex ignores (the agent keeps the session sandbox), and a sync note | | Factory | `tools: read-only` with `mcpServers: []` unless servers are listed. Wins over a portable `tools` list | | Other targets | Sync note | An explicit `x-claude.disallowedTools`, `x-codex.sandbox_mode`, or `x-factory.tools` wins. ## Capabilities {#capabilities} `can` lists what an agent may do, in names that belong to no one tool: ```yaml can: [read(src/**), edit(src/**), shell(go test *), mcp:github] ``` | `can` value | Grants | Claude Code name | |-------------|--------|------------------| | `read`, `read()` | Read files, or matching files | `Read`; the path is dropped | | `write` | Create or overwrite files | `Write` | | `edit`, `edit()` | Edit files, or matching files | `Edit`; the path is dropped | | `delete` | Delete files | No native tool | | `shell` | Run any command | `Bash` | | `shell()` | Run the commands that match the pattern | `Bash`; the pattern is dropped | | `web` | Fetch pages and search the web | `WebFetch`, `WebSearch` | | `mcp:` | Use every tool of one MCP server | `mcp__` | | `mcp:/` | Use one MCP tool | `mcp____` | - Each capability becomes the tool's own name. `delete` raises a sync note on tools with no delete tool. The names match the hook [`match:` tool kinds](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). - Claude Code names work as aliases, and a list can mix both: `can: [read, Grep]`. `Agent()` and `x-claude.tools` are written as given. - Claude Code reads an agent's `tools` by name only, so sync drops a path or pattern there and prints a note such as `claude: .agnostic-ai/agents/code-reviewer.md: shell(git diff *) becomes Bash; Claude Code does not limit an agent's tools to a pattern`. `on-unsupported: error` fails on it. To limit commands there, add a `PreToolUse` hook to the agent's `hooks`, or deny commands in a [settings spec's permissions](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules), which apply to every agent. A skill's `allowed-tools` keeps the pattern. - Claude Code refuses to launch an agent with an empty tool list, such as `can: []` or `can: [delete]`. Sync leaves it out of `.claude/agents/` with a note. - For Claude Code, a server name that contains `__` or ends in `_`, such as `reports__v2`, becomes `mcp____*`, the only whole-server form Claude Code matches for it. - `validate`, `lint` (LINT036), and `sync` stop on an unknown capability, on `can` beside `tools`, and on `can` under `x-`. - A capability is only as strong as the tool's own permission system. agnostic-ai adds no sandbox. `agnostic-ai migrate --only capabilities` rewrites `tools` as `can`; names with no capability of their own stay as aliases. `import` writes capabilities when each tool name matches one. `lint --suggest-capabilities` suggests neutral names for aliases (LINT037), off by default. ## Per-target `model` and `effort` {#per-target-model-and-effort} `model:` and `effort:` each take a single value or a map keyed by target name, with an optional `default`. The first match wins: 1. `x-.` (`null` deletes the key) 2. `.` 3. `.default` 4. Nothing is written, and the tool uses its own default. A target value that is not a single value falls back to `default`. ```yaml --- name: architect description: Designs the change before anyone codes it. model: claude: opus cursor: "claude-opus-5[effort=high]" default: gpt-6.1-sol effort: claude: xhigh qoder: 8000 default: high x-codex: model_reasoning_effort: xhigh --- ``` Claude gets `opus` and `xhigh`. Qoder gets `gpt-6.1-sol` and `8000`. Cursor gets its model id and no effort. Codex gets `gpt-6.1-sol` with effort `xhigh` from `x-codex`. **`effort` values by target.** Without `effort`, the agent uses the session's level. | Target | Values | How it lands | |--------|--------|--------------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | `low`, `medium`, `high`, `xhigh`, `max`, model dependent | `effort`, not checked | | [Qoder](https://agnostic-ai.org/docs/targets/qoder/) | Same five names, or a positive integer | `effort` | | [Junie](https://agnostic-ai.org/docs/targets/junie/) | Alias of `reasoningLevel` | `effort` | | [Factory](https://agnostic-ai.org/docs/targets/factory/) | `low`, `medium`, `high` | `reasoningEffort`; other values raise a sync note | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | Any string | `model_reasoning_effort`; an integer raises a note | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/) | none | Sync note. Put it in the model id, as above | | [Copilot](https://agnostic-ai.org/docs/targets/copilot/) | none | Sync note. `x-copilot.effort` still passes through | | Every other target | none | Sync note | **Claude model names on other targets.** A shared `model` (a single value or `default`) set to a Claude model name is left out, with a sync note, on a tool that cannot load it, so that tool uses its own default. The note suggests `model: {claude: }`, or names the tier to fix when the name sits in a tier's `default`. `on-unsupported: error` fails the sync instead. A value under `model.` or `x-.model` is written as given. `import claude` writes these names as `model: {claude: }`, and `import codex` adds a Codex agent model as `model.codex`. | Target | Claude names that raise the note | |--------|----------------------------------| | [Codex](https://agnostic-ai.org/docs/targets/codex/), [Gemini](https://agnostic-ai.org/docs/targets/gemini/), [OpenCode](https://agnostic-ai.org/docs/targets/opencode/), [Kilo Code](https://agnostic-ai.org/docs/targets/kilo/) | The aliases, `inherit`, and `claude-*` ids | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/), [Factory](https://agnostic-ai.org/docs/targets/factory/), [Kiro](https://agnostic-ai.org/docs/targets/kiro/) | The aliases | Claude Code's [aliases](https://code.claude.com/docs/en/model-config) are `sonnet`, `opus`, `haiku`, `fable`, `best`, `opusplan`, `sonnet[1m]`, and `opus[1m]`. The value `default` resets Claude's model instead of naming one, so it raises no note. ## Model tiers {#model-tiers} A [tier](https://agnostic-ai.org/docs/configuration/#models) names a model role once in `agnostic-ai.yaml`, such as `strong: {claude: opus, codex: sol, effort: high}`. Write `model: strong` in an agent, and each target the tier names gets its model; the others keep their default. For `model`, the first match wins: 1. `x-.model` 2. `model.` in the spec 3. The tier's entry for the target 4. The tier's `default` 5. The tool default To override one target, write the tier as the map's `default`: `model: {codex: gpt-6-luna, default: strong}`. - The tier's `effort` applies only when the spec sets no `effort`, and is skipped for a target whose model the spec sets itself. - A value under `model.` or `x-.model` is never a tier name. `model.` can be a [vendor alias](https://agnostic-ai.org/docs/configuration/#models) such as `codex: sol`. - `explain agents/.md` lists the model and effort each configured tool gets. [`models`](https://agnostic-ai.org/docs/configuration/#models) lists the tier `lint` checks (LINT025, LINT026). - `import claude` suggests a tier when two or more agents set the same Claude model. `import claude` and `import codex` keep `model: strong` when the imported model and effort match what the tier gives that tool. ## Agents as skills {#agents-as-skills} Amp, Crush, Warp, and Zed have no subagents, so sync drops agents there by default. Set `outputs..agents: skill` to write each agent as a skill, which loads only when the model needs it. ```yaml outputs: amp: agents: skill ``` - The skill is `//SKILL.md`, with the agent's `name` and `description`. Its body starts with a note: the model plays the role in the main session and says so. Nothing enforces that separation. - Fields a skill cannot carry, such as `tools` and `model`, are dropped with a note. - An [agent reference](https://agnostic-ai.org/docs/spec-format/#agent-and-skill-references) in skills, agents, and commands becomes that tool's skill phrase. A rule in a shared entry-point file such as `AGENTS.md` keeps the neutral phrase. - An agent and a skill with the same name would share a folder, so `validate` fails on them. - These four tools write `.agents/skills/`, which Codex, Copilot, Gemini, Cline, Cursor, OpenCode, and Junie read too, and several other tools with subagents write there. When one of those is enabled, sync keeps the agents out of that directory and names the tool. Set `outputs..skills-dir` to a separate directory to use this option beside them. - The key cannot be combined with `rules-file` or, on Warp, `workflows-dir`. Other targets reject the key. ## `can` and `tools` support by target {#tools-support-by-target} A tool that cannot honor the list prints a sync note, so `can: [read]` never silently becomes an unrestricted agent. | Target | Behavior | |--------|----------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/), [Copilot](https://agnostic-ai.org/docs/targets/copilot/), [Junie](https://agnostic-ai.org/docs/targets/junie/) | Written as a YAML list | | [Qoder](https://agnostic-ai.org/docs/targets/qoder/), [Trae](https://agnostic-ai.org/docs/targets/trae/) | Written as a comma-separated string (`tools: Read, Bash`) | | [Windsurf](https://agnostic-ai.org/docs/targets/windsurf/), [Kiro](https://agnostic-ai.org/docs/targets/kiro/), [Factory](https://agnostic-ai.org/docs/targets/factory/), [Gemini](https://agnostic-ai.org/docs/targets/gemini/), [Kilo Code](https://agnostic-ai.org/docs/targets/kilo/), [OpenCode](https://agnostic-ai.org/docs/targets/opencode/) | Changed to the tool's own names or a permission map | | [Antigravity](https://agnostic-ai.org/docs/targets/antigravity/), [OpenHands](https://agnostic-ai.org/docs/targets/openhands/), [Goose](https://agnostic-ai.org/docs/targets/goose/), [Codex](https://agnostic-ai.org/docs/targets/codex/), [Cursor](https://agnostic-ai.org/docs/targets/cursor/), [Augment](https://agnostic-ai.org/docs/targets/augment/) | Dropped with a note | On Trae, an explicitly empty list disables all tools, and an unset field loads every tool. Use `x-trae: {tools: ""}` to restrict only Trae. The change can widen access: on Kiro, `edit` also allows `delete_file`. Sync prints a note naming the extra access, and `on-unsupported: error` fails. `explain agents/.md` lists each tool's names and any widening. Most tools accept their own names through `x-.tools`, which skips the change. ## `mcpServers` support by target {#mcpservers-support-by-target} `mcpServers` names the servers, usually from `mcps/`, that one agent may reach. Without it, the agent gets the session's full set. An [agent-only server](https://agnostic-ai.org/docs/spec-format/mcps/#agent-only) reaches only the agents that name it. | Target | Behavior | |--------|----------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/), [Qoder](https://agnostic-ai.org/docs/targets/qoder/) | Server names in the agent file. An agent-only server is written in full instead | | [Kiro](https://agnostic-ai.org/docs/targets/kiro/) | Every named server written in full, under `mcpServers` | | [OpenHands](https://agnostic-ai.org/docs/targets/openhands/) | Every named server written in full, under `mcp_servers`. Not when Goose shares `.agents/agents/`: set `outputs.openhands.agents-dir` | | [Junie](https://agnostic-ai.org/docs/targets/junie/), [Factory](https://agnostic-ai.org/docs/targets/factory/) | Server names in the agent file. An agent-only server's name is left out, except on Junie when it is the only name | | [Antigravity](https://agnostic-ai.org/docs/targets/antigravity/) | Dropped; its docs give no shape for an inline server. Set `x-antigravity.mcpServers` | | [Gemini](https://agnostic-ai.org/docs/targets/gemini/), [Copilot](https://agnostic-ai.org/docs/targets/copilot/) | Dropped. Set `x-gemini.mcp_servers` or `x-copilot.mcp-servers` | | Every other target | Dropped, and agent-only servers left out, with a sync note | An `x-` key for the list, such as `x-kiro.mcpServers`, replaces what sync would write. On Kiro and OpenHands, a name with no MCP spec is dropped with a note. An empty list keeps every server on Junie and excludes every server on Factory, so list the servers you want. ## `maxTurns` and `background` by target {#maxturns-and-background-by-target} A native key under `x-`, such as `x-gemini.max_turns`, `x-kilo.steps`, or `x-cursor.is_background`, wins. An `x-` override with the wrong type raises a sync note, and the shared value is used. `validate`, `lint` (LINT050), and `sync` stop on a `maxTurns` that is not a whole number of 1 or more, or a `background` that is not `true` or `false`. | Target | `maxTurns` | `background` | |--------|------------|--------------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | `maxTurns` | `background` | | [Qoder](https://agnostic-ai.org/docs/targets/qoder/) | `maxTurns` | `background`, when background subagent sessions are on | | [Junie](https://agnostic-ai.org/docs/targets/junie/) | `maxTurns` | Sync note | | [Gemini CLI](https://agnostic-ai.org/docs/targets/gemini/) | `max_turns` | Sync note | | [Kilo Code](https://agnostic-ai.org/docs/targets/kilo/), [OpenCode](https://agnostic-ai.org/docs/targets/opencode/) | `steps` | Sync note | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/) | Sync note | `is_background` | | Every other target | Sync note | Sync note | `import` reads each native key back into `maxTurns` or `background`. When the spec already sets the value under `x-`, import writes it back there and keeps the shared value. ## `permissionMode` and agent `hooks` support by target {#agent-policy-support-by-target} Without either field, the agent uses the session's setting. On Claude Code, a session in `bypassPermissions`, `acceptEdits`, or `auto` mode runs the agent in that mode and ignores its value, and an agent's own `bypassPermissions` never applies. | Target | `permissionMode` | Agent `hooks` | |--------|------------------|---------------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | Seven values; `manual` is an alias of `default` | Written to the agent file | | [Qoder](https://agnostic-ai.org/docs/targets/qoder/) | Six values. Any other value is reported | Seven events. A wider setting is reported | | [OpenHands](https://agnostic-ai.org/docs/targets/openhands/) | Different names (`always_confirm`, `never_confirm`, `confirm_risky`). Set `x-openhands` | Six snake_case events, command handlers only. Set `x-openhands` | On Qoder, `bypassPermissions` becomes `acceptEdits` when security policy disables it, and an agent runs fewer hook events than the project does. ## `color` support by target `color` is written as given and not checked. An unrecognized value only affects looks. | Target | Values | |--------|--------| | [Augment](https://agnostic-ai.org/docs/targets/augment/) | Free text, an ANSI color name | | [Kilo Code](https://agnostic-ai.org/docs/targets/kilo/) | Hex or a theme token, so `color: blue` may not show as expected | | [Claude Code](https://agnostic-ai.org/docs/targets/claude/), [Qoder](https://agnostic-ai.org/docs/targets/qoder/) | One of eight names: `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan` | | [OpenHands](https://agnostic-ai.org/docs/targets/openhands/) | Dropped with a note, since it shares `.agents/agents/` with [Goose](https://agnostic-ai.org/docs/targets/goose/), which has no `color`. Set `x-openhands.color` to a [Rich color name](https://rich.readthedocs.io/en/stable/appendix/colors.html) | # Skills `skills/` holds procedures the agent loads only when a task needs them: cut a release, write a migration, investigate a bug report. The tool keeps each skill's `description` in view and reads the body and bundled files only when the skill applies. Use a [rule](https://agnostic-ai.org/docs/spec-format/rules/) for what applies to every task. ## Write one `agnostic-ai new skill yaml-validator` creates a starter under `.agnostic-ai/skills/`. Use a flat `skills/yaml-validator.md`, or a nested `skills/yaml-validator/SKILL.md` when the skill has files, such as `schema.yaml` beside it. ```markdown --- name: yaml-validator description: Validate YAML against a schema. Use when a YAML file changes or the user asks to check one. --- # YAML Validator 1. Read target file 2. Parse YAML and compare against `schema.yaml` 3. Report violations as `path: message` ``` Run `agnostic-ai lint`, `agnostic-ai sync`, and `agnostic-ai sync --check`. Then ask Claude Code to use `/yaml-validator`, or Codex to use `$yaml-validator`, with a YAML file and its schema. ## Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | no | dir or filename | Skill name and output directory. Some tools restrict the format. | | `description` | no | empty | One line the model uses to decide whether to use the skill. | | `argument-hint` | no | unset | Hint for the arguments to a Claude Code slash command. Other tools leave it out unless you set it under their `x-` block. | | `disable-model-invocation` | no | unset | `true` stops the model from using the skill on its own. A person can still call it. See [support by target](#disable-model-invocation-support-by-target). | | `model` | no | unset | Claude Code model for the rest of the turn. Single value, per-target map, or [tier](https://agnostic-ai.org/docs/spec-format/agents/#model-tiers) name; `x-claude.model` wins. | | `effort` | no | unset | Claude Code effort for the rest of the turn. Single value or per-target map; `x-claude.effort` wins. | | `allowed-tools` | no | unset | Tool-neutral capabilities the skill may use, such as `[read, shell(git diff *)]`. See [allowed tools](#allowed-tools). | | `license` | no | unset | The Agent Skills license, kept in every tool's `SKILL.md`. | | `workspaces` | no | empty | Project directories where Cursor also gets a copy, such as `[apps/web]`. Cursor loads skills only from the workspace it opens, so a session or SDK agent started in `apps/web` misses a root skill. The skill stays at the root for every tool. | A skill's scope comes from its folder. `skills/services/api/review/SKILL.md` puts the skill under `services/api/`, where only sessions in that directory load it. `scope:` in the frontmatter has no effect, and `lint` warns about it (LINT018). `import cursor` writes `workspaces` when a root `.cursor/skills/` links to a skill folder under a project directory. Sync, including global sync, prints a note for each skill field a tool leaves out, such as `argument-hint`, `model`, or `effort`; a field kept in the tool's frontmatter, a command copy of the skill, or a policy file gets none. `agnostic-ai compare claude codex` shows which fields each tool keeps, translates, or drops. Shared global directories leave out per-target overrides. ## Allowed tools {#allowed-tools} Write `allowed-tools` with the same [capabilities](https://agnostic-ai.org/docs/spec-format/agents/#capabilities) as an agent's `can`: ```yaml allowed-tools: [read(src/**), edit(src/**), shell(git diff *), mcp:github] ``` Unlike an agent's tool list, `allowed-tools` keeps paths and patterns, so `shell(git diff *)` pre-approves only those commands. Each tool gets its own names where it supports this field. A tool that drops a restriction or grants more access raises a sync note, and `on-unsupported: error` fails when a tool would grant more access. Claude Code names, scoped ones such as `Read(src/**)` and `Bash(git diff *)` included, work as aliases, and a list can mix both. `migrate --only capabilities`, `import`, and `lint --suggest-capabilities` (LINT037) convert names as they do for [agents](https://agnostic-ai.org/docs/spec-format/agents/#capabilities). Neither `migrate` nor `import` guesses a narrower restriction from a tool's bundle of tools. ## Bundled files and output Only `SKILL.md` and flat `skills/*.md` files are read as skills. Every other file in a nested skill directory is a bundled file (scripts, templates, fixtures, extra `*.md`), copied unchanged to the same relative path under each tool's skills directory. Import and sync keep executable bits. Most tools get `//SKILL.md` with the bundled files. Several share `.agents/skills/`, so identical files are written once. Tools without skill support get the skill as a `skill-.md` rule. Bundled files cannot follow, so they raise a sync note. Set `outputs..emit-skills-as-commands: true` to also write a slash command. Each target page gives the exact directory. A body can point at another skill with [`{{$SKILLS_DIR}}`](https://agnostic-ai.org/docs/spec-format/#path-variables-name), which becomes each tool's own skills directory. ## Claude Code body syntax {#claude-code-body-syntax} Claude Code replaces some syntax in a skill body before the model reads it ([skills docs](https://code.claude.com/docs/en/skills)): - `` !`command` `` lines and ` ```! ` blocks run the command and insert its output. - `$ARGUMENTS` becomes the text typed after the skill name. - `$0`, `$1`, ... and `$ARGUMENTS[N]` become one argument each. Other targets do not expand this syntax in project skills. Sync copies the body as written and prints one note per tool and syntax, naming each line: ``` note: `!`command`` on 1 skill has no effect on codex (the command does not run at .agnostic-ai/skills/pr/SKILL.md:8; put the line in a ::target claude fence) ``` `on-unsupported: error` fails the sync instead, and `silent` hides the note. `lint` reports each line as LINT019. `sync --global` checks the user-level destination and reads `on-unsupported` from the `agnostic-ai.yaml` in the source root. There, Kilo's [inline shell placeholders](https://agnostic-ai.org/docs/targets/kilo/#shell-commands-in-global-skills) in trusted user skill directories pass; argument placeholders and Claude dynamic code blocks still raise notes. Put the Claude line in a fence and give other tools their own text: ```markdown ::target claude !`git log main..HEAD --oneline` ::end ::target codex Run `git log main..HEAD --oneline` first. `$ARGUMENTS` below means the text passed after the skill name. ::end ``` A `$1` or `` !`command` `` inside a fenced code block counts as an example and is not reported. `$ARGUMENTS` is still reported there. Escape a literal dollar as `\$1`, as Claude Code expects. `import claude` keeps the body as written, so Claude Code keeps its dynamic context. [Commands](https://agnostic-ai.org/docs/spec-format/commands/#claude-code-body-syntax) use the same syntax, and a few tools support part of it there. ## `disable-model-invocation` support by target {#disable-model-invocation-support-by-target} Without the field, every tool lets the model use the skill on its own. Each target page gives the native details and links the vendor docs. | Target | Behavior | |--------|----------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/), [Cursor](https://agnostic-ai.org/docs/targets/cursor/), [Zed](https://agnostic-ai.org/docs/targets/zed/) | Written to `SKILL.md` | | [Copilot](https://agnostic-ai.org/docs/targets/copilot/) | Written to `SKILL.md`, with `argument-hint` | | [OpenCode](https://agnostic-ai.org/docs/targets/opencode/) | Written to `SKILL.md`. V2 honors it; V1 does not | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | `allow_implicit_invocation: false` in `agents/openai.yaml` | | [Windsurf](https://agnostic-ai.org/docs/targets/windsurf/) | `triggers: [user]` in `SKILL.md` | | [Crush](https://agnostic-ai.org/docs/targets/crush/) | Written to `SKILL.md` with `user-invocable: true` | | [Factory](https://agnostic-ai.org/docs/targets/factory/) | Dropped with a note. Set `x-factory.disable-model-invocation` | | Cline, Amp, Warp, Antigravity, Continue, Gemini CLI, Goose, OpenHands | Dropped with a note: no documented field keeps a skill user-only | Copilot, Zed, Windsurf, and Crush write it only while no other target writes the same skills folder, since every writer of a shared `SKILL.md` must agree on its bytes. Otherwise the field is dropped with a note; point the tool's `skills-dir` at a folder of its own to keep it. Factory skills go in the shared `.agents/skills/` folder, where the key would reach tools that ignore it and let their models use a manual-only skill. So while another target writes that folder, `x-factory.disable-model-invocation` also needs `outputs.factory.skills-dir: .factory/skills`; otherwise sync stops with `AAI-102`. # Rules `rules/` holds the conventions an agent follows without being asked, such as commit format or test style. Sync writes each rule in every tool's own format: Cursor `.mdc` files, Claude Code rules, `AGENTS.md` sections. Keep a rule short and write it as an instruction; put a multi-step procedure in a [skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Write one `agnostic-ai new rule conventional-commits` creates `rules/conventional-commits.md` with `globs: "**/*"` and `alwaysApply: true`. ```markdown --- name: conventional-commits description: Always use Conventional Commits format. globs: "**/*" alwaysApply: true --- Use `feat:`, `fix:`, `docs:`, etc. Subject under 72 chars. ``` A rule that loads when the agent works on matching files: ```markdown --- name: react-components description: Conventions for React components. globs: "src/**/*.{ts,tsx}" --- Write function components. Keep one exported component per file. ``` A rule for one directory. Create it with `agnostic-ai new rule payments-context --scope services/payments`, or place the file at `rules/services/payments/payments-context.md`: ```markdown --- name: payments-context scope: services/payments --- Use integer minor units for monetary values. ``` Claude Code gets a rule that applies only there. Codex gets `services/payments/AGENTS.md`. Cursor gets its own `.mdc` rule, unless a tool like Codex needs the shared `AGENTS.md`, which Cursor then reads too. Gemini gets `services/payments/GEMINI.md`. [Directory-specific instructions](https://agnostic-ai.org/docs/scoped-context/) lists every tool and the selector limits. ## Headings in merged files When several rules share a file, such as Codex's `AGENTS.md` or Gemini's `GEMINI.md`, each rule gets a `### ` section, and sync moves the body's headings down together so its top heading is at least `####`. A body with `### Doc versioning` and `#### Details` becomes `#### Doc versioning` and `##### Details`. - The `outputs.claude.rules-file` layout writes `## ` sections, so there the top body heading is at least `###`. - Headings already deep enough keep their levels. No heading goes past `######`, so when a body's deepest heading is more than two levels below its top one (three in the Claude layout), the deepest levels merge at `######`, and import cannot separate them again. - The section's source comment records the shift, as in ``, and import moves the headings back. ### Edge cases for heading shifts - Fenced code (including fences inside lists) and raw HTML blocks keep their headings as written. - Link reference definitions stay separate from underlined (Setext) headings. - List items followed by a thematic break remain lists. - Raw HTML blocks follow CommonMark. A line such as `Note: read first` is paragraph text, so a `---` under it forms a heading that nests too. - A rule file written on its own, or a nested file with one rule and no section wrapper, keeps its heading levels. - An underlined (Setext) heading comes back from import as a `#` heading. Hard line breaks in its text become `
` tags. - Headings and source-comment examples inside fenced code and raw HTML stay in their rule body. They do not start another rule. - When a rule leaves a code or HTML block open, its source comment also records `body-lines: `, which sync updates and import uses to find the next section. ## Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | no | filename | Rule identifier. | | `description` | no | empty | Short summary. | | `scope` | no | project-wide | A project-relative directory and everything under it. It wins over the rule's folder. A folder that names a project directory scopes a rule without `scope`; any other folder only groups rules. A scope inside `node_modules` is refused. See [scoped context](https://agnostic-ai.org/docs/scoped-context/). | | `globs` | no | depends on the tool; `new rule` seeds `**/*` | Project-relative patterns, as a comma-separated string (`"*.go,*.mod"`) or a list. A comma inside a brace set does not separate patterns, so `"src/**/*.{ts,tsx}"` is one pattern. With `scope`, applies to the whole scope directory and every matching file. `new rule --scope` omits it. | | `paths` | no | unset | Another name for `globs`, the form Claude Code writes. Every tool reads it as `globs`. Set only one of them; `validate` rejects a rule whose `paths` and `globs` name different patterns. See [selector limits](https://agnostic-ai.org/docs/scoped-context/#narrow-a-rule-to-certain-files). | | `alwaysApply` | no | `false` with `globs`, else depends on the tool; `new rule` seeds `true` | Asks the tool to always load the rule. Unset on a rule with `globs`, it is `false` in every tool that has an activation field, so the rule loads only on matching files; a catch-all such as `**/*` keeps it always-on. With `scope`, the rule stays within the scope plus patterns. `new rule --scope` omits it. | A rule with neither `globs` nor `alwaysApply` loads differently per tool; each target page says when. Set `alwaysApply: true` when a rule must load everywhere. # Commands `commands/` holds slash commands: saved prompts a person starts by name, such as `/deploy`. The body is the prompt the tool sends. Use a [skill](https://agnostic-ai.org/docs/spec-format/skills/) instead when the model should pick the workflow itself, or when it needs bundled files. Set `outputs..emit-skills-as-commands: true` to get both from one skill. ## Write one ```bash agnostic-ai new command review-changes ``` It creates `.agnostic-ai/commands/review-changes.md`. Write the prompt, run `agnostic-ai sync`, then type `/review-changes` in a supported tool. ```markdown --- name: review-changes description: Review the current branch against main. x-claude: allowed-tools: [Bash(git diff:*), Read, Grep] --- Diff the branch against `main` and report bugs with `file:line`, most severe first. ``` ## Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | no | filename | Command identifier and slash name, such as `/deploy`. | | `description` | no | empty | One-liner shown in slash-command pickers. | | `argument-hint` | no | empty | Hint shown after the command in Claude Code, Augment, and Factory. | Other frontmatter is kept. Put keys for one tool under `x-`, like `x-claude.allowed-tools` above. ## Output Each target page lists the exact directory. Gemini writes each command as a `.toml` file named after `name`. Codex without `outputs.codex.commands-dir`, Copilot, Zed, Windsurf, and Crush get each command as a skill ([below](#tools-without-commands)). Other tools without command support skip it with a note. ## Tools without commands {#tools-without-commands} A tool that has skills but no commands gets each command as a skill only the user starts, when the tool can keep the model from starting it: [Codex](https://agnostic-ai.org/docs/targets/codex/), [Copilot](https://agnostic-ai.org/docs/targets/copilot/), [Zed](https://agnostic-ai.org/docs/targets/zed/), [Windsurf](https://agnostic-ai.org/docs/targets/windsurf/), and [Crush](https://agnostic-ai.org/docs/targets/crush/). All but Codex qualify only while no other tool writes their skills folder, since a shared `SKILL.md` cannot keep the field. Cline, Amp, Warp, Antigravity, Continue, Goose, and OpenHands document no such switch, so they skip commands. The skill: - is written at the top level of the skills folder, whatever folder the command sits in under `commands/`; - sets `disable-model-invocation: true`, which Codex reads as `allow_implicit_invocation: false` in `agents/openai.yaml`, Copilot and Zed keep in `SKILL.md`, Windsurf writes as `triggers: [user]`, and Crush writes with `user-invocable: true`; - keeps `name`, `description`, the target fields, and `argument-hint` where the tool reads it, and notes any other field it leaves out. A target field that decides who starts the skill, `disable-model-invocation`, `triggers`, or `user-invocable`, is left out, so the skill stays user-run; - opens with one line asking the model to run it only when called, and to treat the text typed after its name as its arguments, since these tools do not fill in `$ARGUMENTS` in a skill. Sync notes each `$ARGUMENTS` or `$1` line, and `on-unsupported: error` does not fail on them. A skill spec with the same name (ignoring case), or a hand-written skill folder of that name the tool reads, wins, and sync notes the skipped command. A tool that reads the same skills folder, such as Gemini CLI reading the copy Codex writes to `.agents/skills`, also sees the skill, even with `target-exclude`; sync notes that it lists the command twice, or that its model can start it. ## Claude Code body syntax {#claude-code-body-syntax} A Claude Code command body can use `` !`command` `` lines, ` ```! ` blocks, `$ARGUMENTS`, and `$1`, `$2`, ... ([skill syntax](https://agnostic-ai.org/docs/spec-format/skills/#claude-code-body-syntax)). Sync copies the body as written. Support follows each tool's docs: | Target | `` !`command` `` | `$ARGUMENTS` | `$1`, `$2`, ... | |--------|------------------|--------------|-----------------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | yes | yes | yes | | [OpenCode](https://agnostic-ai.org/docs/targets/opencode/) | yes | yes | yes | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | no | yes | yes | | [Augment](https://agnostic-ai.org/docs/targets/augment/), [Factory](https://agnostic-ai.org/docs/targets/factory/) | no | yes | no | | [Kiro CLI V3](https://agnostic-ai.org/docs/targets/kiro/#commands) | no | yes | no | | Cursor, Gemini, Junie, Kilo, Qoder, Trae | no | no | no | A tool that reads the syntax as plain text gets a note naming each line; `on-unsupported: error` fails the sync, and `lint` reports LINT019. Write a tool's own placeholders in a `::target` fence: Gemini's argument and shell forms ([custom commands](https://geminicli.com/docs/cli/custom-commands)), or Kiro's `${1}` through `${10}` and `${@}`, which sync keeps as written (it does not map a bare `$1`). # Hooks `hooks/` holds commands the AI tool runs at fixed points: when a session starts, before a tool call, after a file edit, when the agent stops. Hooks must be enabled and trusted in that tool. ## A typical session A session contains prompts, and a prompt can lead to several tool calls. The coding tool runs a hook when it reaches a supported event. A hook can report state, check an action, or run a command such as a formatter. `after-edit` is the file-edit case of the after-tool event. It shares that point with `after-tool`. Do not rely on an order between separate hooks there. `stop` means the agent is about to finish a response. Another prompt can follow in the same session; `session-end` is separate. Each tool supports a different set of events. Check the [portable event mappings](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) before relying on a hook. A blocked `before-tool` call does not reach the successful tool result shown in this example. ## Common shared hooks Start with one small hook for a repeated task. Use portable `on` names so sync can translate it for the tools that support the event. | Use | Portable event | A small first step | |---|---|---| | Show the current work | `session-start` | Run `git status --short` when a session opens. | | Check a prompt | `prompt-submit` | Check required input before the agent starts work. | | Guard a command | `before-tool`, `match: shell` | Use the [force-push guard](https://agnostic-ai.org/docs/spec-format/hook-recipes/#shared-hook-scripts). | | Record tool results | `after-tool` | Append a short local log after a matching tool call. | | Format edited files | `after-edit` | Read the [edited paths](https://agnostic-ai.org/docs/spec-format/hook-reference/#edited-paths), then format only those files. | | Check before a reply ends | `stop` | Check a small completion condition; exit 2 can keep the agent working. | | Clean up session files | `session-end` | Remove temporary files that the session created. | Keep a shared script under `.agnostic-ai/scripts/` and reference it from `command`; see [shared hook scripts](https://agnostic-ai.org/docs/spec-format/hook-recipes/#shared-hook-scripts) for paths and platform details. Test guards with both allowed and blocked inputs, and give a `stop` check a clear way to succeed so it does not keep the agent working without an end. ## Write one Create the source file, then replace its contents with the spec below: ```bash agnostic-ai new hook session-status ``` `.agnostic-ai/hooks/session-status.yaml`: ```yaml name: session-status description: Show the working changes when a session starts. targets: [claude, codex] on: session-start command: git status --short ``` It reports Git state and blocks nothing. From a Git project, sync and test it: ```bash agnostic-ai lint agnostic-ai sync agnostic-ai hook run session-status agnostic-ai sync --check ``` For a guard, use `on: before-tool` and a script that reads event JSON on stdin: exit 0 allows the call, and exit 2 blocks it with stderr as the reason. Read the [event and failure contract](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) before you rely on a guard. Hook scripts run with your tools' access, so review hook specs like code. Sync never runs a hook; the tools do, and [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) runs one when you ask. [`agnostic-ai compare claude cursor`](https://agnostic-ai.org/docs/cli-reference/inspect/#compare) shows how each tool writes hook events, matchers, commands, timeouts, and failure policies, and names missing mappings, target exclusions, and ignored options. ## Choose the next step - [Hook reference](https://agnostic-ai.org/docs/spec-format/hook-reference/): portable events, native handlers, fields, scripts, and event input. - [Hook recipes](https://agnostic-ai.org/docs/spec-format/hook-recipes/): the demo force-push guard and shared script locations. - [Test a hook](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run): sample payloads, results, and limits of the diagnostic. - [Target pages](https://agnostic-ai.org/docs/targets/): discovery, trust, and native hook behavior. # Hook reference ## Portable events {#portable-events} Write `on` and `match` instead of one tool's own event name. Sync writes each tool's native event and matcher, so one spec runs on every tool that has a mapping. ```yaml name: no-force-push description: Block git push --force. on: before-tool match: shell command: .agnostic-ai/scripts/no-force-push.sh ``` `on` takes `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, or `session-end`. `after-edit` runs after a file edit, so it takes no `match`. `match` takes a tool kind: `shell`, `edit`, `read`, `web`, `any`, or `mcp:`. It applies only to `before-tool` and `after-tool`. Leave it out, or write `any`, to run on every tool. `edit` covers every edit tool, including ones some versions lack, so a guard misses none. A tool gets only the events where exit codes work as in Claude Code: exit 0 goes on, and exit 2 blocks with stderr as the reason. `before-tool` blocks the tool call, `prompt-submit` blocks the prompt, and `stop` keeps the agent working. `after-tool` and `after-edit` map to the tool's after-tool event, and `any` writes no matcher. Copilot only warns on exit 2 outside `before-tool`, so it gets the session events and `before-tool`. | Tool | `on` | `match` | |---|---|---| | CodeBuddy Code | `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, `session-end` | `shell` `^Bash$`, `edit` `^(Edit\|Write)$`, `read` `^Read$`, `web` `^(WebFetch\|WebSearch)$`, `mcp:` `^mcp____.*$` | | Claude Code | `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, `session-end` | `shell` `Bash`, `edit` `Edit\|MultiEdit\|Write\|NotebookEdit`, `read` `Read`, `web` `WebFetch\|WebSearch`, `mcp:` `mcp____.*` | | Codex | `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, `session-end` | `shell` `Bash`, `edit` `Edit\|Write`, `mcp:` `mcp____.*` | | Gemini | `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, `session-end` | `shell` `^run_shell_command$`, `edit` `^(write_file\|replace)$`, `read` `^(read_file\|read_many_files)$`, `web` `^(web_fetch\|google_web_search)$` | | Qwen Code | `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, `session-end` | `shell` `^run_shell_command$`, `edit` `^(write_file\|edit)$`, `read` `^read_file$`, `web` `^(web_fetch\|web_search)$`, `mcp:` `mcp____.*` | | Factory | `session-start`, `prompt-submit`, `before-tool`, `after-tool`, `after-edit`, `stop`, `session-end` | `shell` `^Execute$`, `edit` `^(Create\|Edit\|ApplyPatch)$`, `read` `^Read$`, `web` `^(FetchUrl\|WebSearch)$` | | Qoder | `session-start`, `prompt-submit`, `before-tool`, `stop`, `session-end` | `shell` `Bash`, `edit` `Edit\|Write\|NotebookEdit`, `read` `Read`, `web` `WebFetch\|WebSearch`, `mcp:` `mcp____.*` | | OpenHands | `session-start`, `prompt-submit`, `before-tool`, `stop`, `session-end` | `shell` `terminal` | | Goose | `session-start`, `before-tool`, `stop`, `session-end` | `shell` `^shell$`, `edit` `^(write\|edit)$` | | Augment | `session-start`, `before-tool`, `session-end` | `shell` `^launch-process$`, `edit` `^(str-replace-editor\|save-file)$`, `web` `^(web-fetch\|web-search)$` | | Crush | `before-tool` | `shell` `^bash$`, `edit` `^(edit\|multiedit\|write)$` | | Windsurf | `prompt-submit`, `before-tool`, `stop` | `shell` `^exec$`, `edit` `^(edit\|write\|apply_patch)$`, `read` `^read$`, `web` `^(webfetch\|web_search)$` | | Cursor | `session-start`, `prompt-submit`, `before-tool`, `session-end` | `shell` `^Shell$`, `edit` `^Write$`, `read` `^Read$` | | Copilot | `session-start`, `before-tool`, `session-end` | `shell` `Bash`, `edit` `Edit\|Write`, `read` `Read`, `web` `WebFetch\|WebSearch` | | Cline | `before-tool` | `shell` `run_commands\|execute_command`, `edit` `editor\|apply_patch\|replace_in_file\|write_to_file`, `read` `read_files\|read_file`, `web` `fetch_web_content\|web_fetch\|web_search` | | Kiro | `before-tool` | `shell` `^(execute_bash\|execute_pwsh)$`, `edit` `^(fs_write\|fs_append\|str_replace\|delete_file)$`, `read` `^(read_file\|list_directory\|file_search\|grep_search\|code\|tool_search\|introspect)$`, `web` `^(web_fetch\|remote_web_search)$`, `mcp:` `@/*` | Cursor, Copilot, and Cline signal a block in their own way. Sync runs each portable `before-tool` command (and, on Cursor, each `prompt-submit` command) through a bash wrapper that turns exit 2 into the tool's deny reply, with stderr as the reason. Exit 1 differs: Cursor goes on, as Claude Code does; Copilot denies the call, so a guard with a typo or a missing dependency keeps blocking; Cline ignores it with no report. On Windows, where the tool runs hook commands through PowerShell, the wrapper cannot start: Cursor goes on with an error, and Copilot denies the call. A hook written with `event` gets no wrapper. See [Cursor](https://agnostic-ai.org/docs/targets/cursor/#hooks), [Copilot](https://agnostic-ai.org/docs/targets/copilot/#hooks), and [Cline](https://agnostic-ai.org/docs/targets/cline/#hooks) for the reply each wrapper writes. On Cursor, the deny reply carries stderr as `user_message` and `agent_message`, or on `prompt-submit` is `"continue": false` with `user_message`. At exit 0 the wrapper passes on the JSON reply the command prints. Cursor gets no `after-tool`, `after-edit`, or `stop`, because exit 2 on `postToolUse`, `afterFileEdit`, and `stop` is a failure Cursor moves past. Cursor does not wait for `session-start` or `session-end`, and does not add `session-start` stdout to the session as Claude Code does. A spec sets `on` or `event`, never both; `match` goes with `on`, and `matcher` with `event`. `validate` and `lint` (LINT032) report an unknown value, both forms mixed, an event that a reached tool reads differently (such as `on: stop` on Crush), and a tool kind a reached tool lacks (such as `match: read` on Codex). Sync reports missing mappings. For a tool with no mapping, write `event` for the hook, or limit it with `targets`. Trae has no portable hook mapping yet. Kiro maps only `on: before-tool`: CLI V3 cannot block on `after-tool` or `prompt-submit`, and its `stop` uses exit 1 to keep the agent running. `decision: stdout` has no Kiro mapping. The `edit` matcher leaves out `code`, which Kiro's own `write` category includes. See [Kiro](https://agnostic-ai.org/docs/targets/kiro/) for how tool IDs and `mcp:` map. `agnostic-ai migrate --only hooks` rewrites `event` and `matcher` as `on` and `match` when the portable form gives every reached tool the same event and matcher, and says why it leaves any other hook as written. A Claude Code hook on `Edit|Write` stays native, since `match: edit` there also runs on `MultiEdit` and `NotebookEdit`. `lint` warns on each hook the migration would rewrite (LINT034), and `agnostic-ai import` follows the same rule. ### Decision on stdout {#decision-on-stdout} Set `decision: stdout` on a `before-tool` hook to decide with a JSON object instead of an exit code: ```yaml name: no-force-push on: before-tool match: shell decision: stdout command: .agnostic-ai/scripts/no-force-push.sh ``` ```sh echo '{"decision": "deny", "reason": "Use --force-with-lease."}' ``` Stdout carries only the decision, so print logs to stderr. At exit 0: - `"decision": "deny"` blocks the call with `reason` as the message, the same as exit 2 with that message on stderr. - `"ask"` blocks too, since not every tool can ask the user. - `"allow"`, or empty stdout, goes on as a plain exit 0. It never grants more than the tool's own permission rules do. - Anything else blocks, so a broken guard never lets a call through: stdout that is not one JSON object, an object with no top-level `decision` or more than one, or another `decision` value. Only the top level counts, so a nested `decision` never overrides the verdict. Raw NUL and SOH bytes block, and so does stdout over 1,000,000 bytes, more than 10,000 values, or more than 64 nested containers. A `\u` escape outside ASCII reads as `?`. A non-zero exit keeps its usual meaning. No tool reads this object itself: on every tool, each `x-gemini.hooks` command handler included, sync runs the command through a bash wrapper that turns the decision into that tool's block. So `decision: stdout` cannot go with `commandWindows` or `shell: powershell`. Augment runs only a bare script path, so it cannot run the wrapper, and `validate` names a `decision: stdout` hook that reaches it. ### Hooks Cursor and Copilot also read {#claude-settings-copies} Cursor and Copilot also run `.claude/settings.json` hooks. When `cursor` is a target, sync puts `[ "$AGNOSTIC_AI_TARGET" = cursor ] && exit 0;` before the Claude Code copy of each portable hook that also reaches Cursor. Cursor's `sessionStart` hook sets that variable, so the hook runs once there, as Cursor's own copy. Claude Code sets it to `claude`, and an unset variable runs the hook. A hook still runs twice on Cursor when it fires before Cursor's `sessionStart` hook returns, when it has `args` or `shell: powershell` (no POSIX shell to run the check), or when it is a user hook from `sync --global`, which adds no check. Copilot sets no variable, so a hook synced to `claude` and `copilot` [runs twice there](https://agnostic-ai.org/docs/targets/copilot/#hooks); both copies block on exit 2. ## Native events `event` and `matcher` take a tool's own names. This hook formats the Go files the agent edits, on Claude Code and Codex alike: ```yaml name: gofmt-on-edit description: Format the Go files the agent edits. targets: [claude, codex] event: PostToolUse matcher: Edit|Write command: 'files=$(agnostic-ai hook paths) || exit 1; printf "%s\n" "$files" | grep "\.go$" | while IFS= read -r f; do gofmt -w "$f"; done' ``` Codex takes `Edit` and `Write` as aliases for `apply_patch`, so the one matcher fires on both tools. [`agnostic-ai hook paths`](https://agnostic-ai.org/docs/spec-format/hook-reference/#edited-paths) reads the edited files from the event JSON on stdin and must be on the hook's `PATH`. The `|| exit 1` fails the hook when `hook paths` fails, for example on bad JSON or a missing binary; without it, the loop would exit 0. Command hooks receive event JSON on stdin; read the shell command from `tool_input`. `AGNOSTIC_AI_TARGET` names the tool that ran the hook; see [which target ran a hook](https://agnostic-ai.org/docs/spec-format/hook-reference/#hook-target). OpenCode runs hooks as plugins; see [OpenCode V2 hooks](https://agnostic-ai.org/docs/targets/opencode/#opencode-v2-hooks) for the V1 and V2 runtimes. Sync keeps hook entries you wrote. In `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.gemini/settings.json`, `.qoder/settings.json`, and `.factory/hooks.json`, it puts its own entries after yours in each event and changes or removes only those. A hand-written entry that matches a spec's output, with the same matcher (for example after `import`), counts as the spec's own, so it is not written twice. ## Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | no | filename | Hook identifier. | | `description` | no | empty | Free-form documentation. | | `on` | `on` or `event` | none | Portable event, written as each tool's native event. See [portable events](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). | | `match` | no | every tool | Tool kind for `on: before-tool` or `after-tool`. See [portable events](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). | | `decision` | no | none | `stdout` reads a `{"decision": ..., "reason": ...}` object at exit 0 on `on: before-tool`. See [decision on stdout](https://agnostic-ai.org/docs/spec-format/hook-reference/#decision-on-stdout). | | `event` | `on` or `event` | none | Hook event, written as is. See [events](https://agnostic-ai.org/docs/spec-format/hook-reference/#events). | | `matcher` | no | empty | Regex on the tool name, or another selector the event uses. | | `command` | command handlers only | none | Shell command, or a list where each entry becomes its own handler. | | `args` | no | empty | **Exec form**: `command` runs as a program with these arguments and no shell, so spaces, `$`, and backticks pass through. Leave unset when the command needs a pipe or `&&`. Claude Code, Qoder, Copilot, and OpenCode V2 on macOS and Linux keep `args` apart; other tools append them to `command`, each quoted for a POSIX shell, which Trae's PowerShell and OpenHands' cmd.exe on Windows can read differently (sync notes it). Augment skips a hook with `args`, with a note. | | `type` | no | `command` | `command`, `http`, `mcp_tool`, or `prompt`, where the tool supports it. | | `timeout` | no | none | Seconds before the tool cancels the hook. Tool defaults are in the [shell and timeout table](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run). | | `disabled` | no | `false` | Keep the hook defined but stop it running. Antigravity and Kiro write `enabled: false`; OpenCode and Kilo write no plugin module; other tools write the hook unchanged. | Handler-specific fields are written only where the tool's schema defines them: - `server`, `tool`, `input` (`type: mcp_tool`): Claude Code, Codex. - `url`, `headers`, `allowedEnvVars` (HTTP handler): Claude Code, Qoder, Copilot. - `prompt`, `model` (prompt handler): Claude Code, Qoder, Cursor, Copilot (`sessionStart` only). CodeBuddy takes `prompt` without `model`, on `Stop`, `UserPromptSubmit`, and `PreToolUse`. - `statusMessage`, `async`: Claude Code, Codex, Qoder. - `asyncRewake`, `shell`, `if`: Claude Code, Qoder. - `continueOnBlock`: Claude Code, CodeBuddy. `commandWindows`: Codex, Copilot. `additionalContextLimit`: Codex. `loop_limit`: Cursor, Trae. - `failClosed`: Claude Code (command and HTTP handlers), Cursor. `true` blocks when the hook cannot start, times out, or exits with a code other than 0 or 2; Cursor also blocks on exit 0 with no output. See [Claude Code hook mappings](https://agnostic-ai.org/docs/targets/claude-reference/#hook-mappings) for the events it ignores there. - `x-goose.on_failure` (Goose), `x-kiro.action` (Kiro), `x-gemini.hooks`, `x-gemini.sequential`, `x-gemini.name`, `x-gemini.env` (Gemini). `command` is not needed for a non-command handler, a valid `x-kiro.action`, or a hook that sets `x-gemini.hooks`. A hook with none of these, and no `command` under an `x-` block, runs nothing, so `validate` and `lint` (LINT052) report it. Limit a non-command hook to the tools that support it with `target` or `targets`. ## Events `event` is written as is, never translated between tools; for one spec across tools, use [portable events](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). Claude Code and Codex share `PreToolUse`, `PostToolUse`, and `UserPromptSubmit`. Other tools need their own names, such as Cursor's `beforeShellExecution` or Gemini's `BeforeTool`. - `agnostic-ai validate` flags an event a tool does not recognize. - `sync` and `sync --check` stop before writing when an event looks like a typo of a known one, such as `PreToolUze`, and name the closest. - Tools without hook support log a warning and skip the hook. ## Imported project-root paths A shell-form command imported from Claude Code can use `$CLAUDE_PROJECT_DIR` or `${CLAUDE_PROJECT_DIR}` in a script path: ```yaml name: guard event: PreToolUse command: 'sh "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh"' ``` Claude Code, Cursor, and Trae provide the variable, so sync keeps it. Gemini, Qoder, and Factory get their own project-root variable. Other tools get `$(git rev-parse --show-toplevel)` in a POSIX shell, and Codex gets it in `commandWindows` too. When the configuration lives in a subdirectory of a Git worktree, the written path includes that subdirectory, so a hook started from a deeper directory still finds the project. Script paths also move to the tool's hook directory, such as `.claude/hooks/` to `.codex/hooks/`; custom script paths keep their directory. `sync --global` uses the Git root at run time, so it needs Git and a hook running inside the intended worktree, and cannot tell apart several configured projects in one worktree. Exec-form `args`, escaped dollars, and single-quoted variables stay literal. A note names the hook when the tool cannot keep a parameter operator such as `${CLAUDE_PROJECT_DIR:-fallback}` or ${#CLAUDE_PROJECT_DIR}, a nested substitution or here-document, non-POSIX root syntax, or a project without a Git worktree. `on-unsupported: error` fails the sync; `silent` hides the note. For these cases, write a command for the specific tool or set `target: claude`, and set the tool's Windows command yourself. ## Which target ran a hook {#hook-target} A shared script reads `AGNOSTIC_AI_TARGET` to pick how to reply (Claude Code and Codex read exit 2 and stderr; Cursor reads JSON). ```sh case "$AGNOSTIC_AI_TARGET" in cursor) echo '{"permission":"deny","agent_message":"Blocked."}' ;; *) echo "Blocked." >&2; exit 2 ;; esac ``` A spec that sets `AGNOSTIC_AI_TARGET` in its own `env` keeps that value, except on OpenCode V2. Where sync cannot set it, the parent process's value stays. How sync sets it: | Target | How | |---|---| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | `env` in `.claude/settings.json` (`~/.claude/settings.json` for `sync --global`), for the whole session, so the Bash tool sees it too | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/) | A `sessionStart` hook returns it, so `sessionStart` hooks and hooks that fire before it returns do not see it | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | `export AGNOSTIC_AI_TARGET=codex; ` before `command`; not on Windows (`commandWindows`), and only with a POSIX session shell (a `pwsh` or `nu` login shell breaks it) | | [Gemini](https://agnostic-ai.org/docs/targets/gemini/), [Qoder](https://agnostic-ai.org/docs/targets/qoder/), [Copilot](https://agnostic-ai.org/docs/targets/copilot/) | `env` on each command handler | | [Goose](https://agnostic-ai.org/docs/targets/goose/), [Crush](https://agnostic-ai.org/docs/targets/crush/), [Cline](https://agnostic-ai.org/docs/targets/cline/) | An `export` prefix or line | | [OpenCode](https://agnostic-ai.org/docs/targets/opencode/) V1, [Kilo](https://agnostic-ai.org/docs/targets/kilo/) | `.env()` on each plugin command; OpenCode V2 sets `opencode` in each child process's environment | | [Zed](https://agnostic-ai.org/docs/targets/zed/) | `env` on each task | Trae, Factory, OpenHands, Antigravity, Kiro, Windsurf, and Augment do not get the variable. Tell them apart by their own variables, such as `TRAE_PROJECT_DIR`, `FACTORY_PROJECT_DIR`, `OPENHANDS_PROJECT_DIR`, `DEVIN_PROJECT_DIR`, or `AUGMENT_PROJECT_DIR`. `CLAUDE_PROJECT_DIR` does not identify Claude Code, because Cursor and Trae provide it too. `sync --global` leaves a matching hand-written hook alone, so an adopted Codex, Gemini, or Qoder entry does not get the variable. Copilot reads no `env` from `.claude/settings.json`, so its copy of a Claude Code hook gets no value. ## Read the edited paths {#edited-paths} `agnostic-ai hook paths` reads the event data a hook gets on stdin and prints each file the edit leaves on disk, one per line, relative to the directory the hook runs in (a path outside it prints in full). A tool call that is not an edit prints nothing. ```sh agnostic-ai hook paths # src/app.go agnostic-ai hook paths --action # updatesrc/app.go agnostic-ai hook paths --json # [{"action": "update", "path": "src/app.go"}] ``` - The tool comes from `AGNOSTIC_AI_TARGET`, or from `--target`, which wins. Codex on Windows (`commandWindows`) gets no `AGNOSTIC_AI_TARGET`, so pass `--target codex` there. - With no tool named, event data whose `tool_input` holds `file_path`, `edits`, or `notebook_path` reads as Claude Code, which covers Cursor and Copilot running `.claude/settings.json` hooks. Other event data with no tool named fails, as do invalid JSON and an edit tool's `tool_input` that is not an object (exit 1). - A plain run skips deleted files and the source of a move, so a formatter sees only files that exist. `--action` and `--json` list every change as `add`, `update`, `delete`, or `move`; a move shows as a `delete` of its source and a `move` of its destination, which `--json` gives a `from`. - When a tool does not say whether a write created the file, the change shows as `update`. - A hook that runs before the edit finds no new file on disk, so run formatters after the edit. | Target | What it reads | |---|---| | Claude Code | `tool_input.file_path` of `Edit`, `Write`, and `MultiEdit`; `tool_input.notebook_path` of `NotebookEdit` | | Codex | The `*** Add File:`, `*** Update File:`, `*** Delete File:`, and `*** Move to:` headers in the `apply_patch` text in `tool_input.command`, so every file in the patch | | Cursor | `file_path` of `afterFileEdit` and `afterTabFileEdit` only; other Cursor events print nothing | | Gemini | `tool_input.file_path` of `write_file` and `replace`, a relative path starting at `cwd` | | Factory | `tool_input.file_path` of `Create` and `Edit`; a Factory `ApplyPatch` prints nothing, since its input is undocumented | | Qoder | `tool_input.file_path` of `Write`; `Edit` fails, since the docs show none of its `tool_input` fields | | Augment | `file_changes[].path` with its `changeType`; before the edit, `tool_input.path` of `str-replace-editor` and `save-file` | Factory and Augment do not get `AGNOSTIC_AI_TARGET`, so give their hooks their own spec and pass `--target`. ### Tools the command does not read Their docs do not show an edit's event data, so the command fails: - Windsurf, Trae, Copilot, Goose, Antigravity, OpenHands, Cline. - Kiro: a `PostFileSave` command can use its `filePath` template variable. - Crush: tool events set `CRUSH_TOOL_INPUT_FILE_PATH`. See [native lifecycle events and matcher limits](https://agnostic-ai.org/docs/targets/crush/#hooks). - OpenCode V1 and Kilo: plugins get tool arguments as JavaScript objects, not as event data on stdin. OpenCode V2 sends native event JSON, which this command does not read. - Zed: no hook fires on an edit. ## Spec guard hook {#spec-guard} `agnostic-ai init --demo` seeds two hooks that check your specs while the agent works, on Claude Code, Codex, and Gemini: - `spec-guard-edit` runs after each edit. When the agent edits a spec, it sees the lint errors in that file on its next turn. - `spec-guard-stop` runs when the agent stops. When specs changed without a sync, the agent sees one line: `Specs changed since the last sync: run agnostic-ai sync.` It never runs sync itself, and `sync --check` still catches a hand edit to a generated file. ```yaml name: spec-guard-edit targets: [claude, codex, gemini] on: after-edit command: 'command -v agnostic-ai >/dev/null 2>&1 || exit 0; agnostic-ai hook guard after-edit' ``` Both fail open: with `agnostic-ai` missing from `PATH`, or event data [`hook guard`](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-guard) cannot read, they exit 0 with no output. Other tools keep the commit-time check of [`install-hook`](https://agnostic-ai.org/docs/cli-reference/maintain/#install-hook). Factory runs `after-edit` and `stop` but does not document its hook shell, so the hooks leave it out. Gemini on Windows runs hook commands in Windows PowerShell, which cannot parse the `command -v` line, so each run there fails with a non-blocking error; remove `gemini` from the hooks' `targets` on Windows. # Hook recipes ## Shared hook scripts Keep one script under `.agnostic-ai/scripts/` and use that path in `command`. `agnostic-ai init --demo` seeds this guard, which blocks `git push --force` and allows `--force-with-lease` on Claude Code and Codex: ```yaml name: no-force-push description: Block git push --force and allow --force-with-lease. targets: [claude, codex] event: PreToolUse matcher: Bash command: '"$CLAUDE_PROJECT_DIR/.agnostic-ai/scripts/no-force-push.sh"' commandWindows: '$s = "$CLAUDE_PROJECT_DIR/.agnostic-ai/scripts/no-force-push.sh"; if (-not (Get-Command sh -ErrorAction SilentlyContinue)) { exit 1 }; sh $s; exit $LASTEXITCODE' timeout: 10 ``` The script reads `tool_input.command` from the event JSON on stdin, with no `jq`, and splits it into words the way `sh` does. On a force push it prints the reason on stderr and exits 2. - `--force` or `-f` blocks, even next to `--force-with-lease`. - A `+main` refspec or `--mirror` counts as forced unless the push uses `--force-with-lease`. - A force push written outside quotes blocks, even behind a wrapper the script does not parse, such as `xargs`. - A `git push --force` inside quotes, a comment, or a heredoc body passes, and so does a quoted command inside `bash -c` or `eval`. Both tools start a hook in the session directory, which can be below the project root, so the script path starts at the [project root](https://agnostic-ai.org/docs/spec-format/hook-reference/#imported-project-root-paths). On Windows, Codex runs `commandWindows` with `powershell.exe -Command`, and the command above handles three PowerShell quirks: - PowerShell ignores the script's `#!/bin/sh` line, so the command names `sh`. That needs `sh` on `PATH`, as Git for Windows provides. - PowerShell reports a failed native command as exit 1, which Codex lets through. `exit $LASTEXITCODE` passes on the script's exit 2. - The Git root lookup resets `$LASTEXITCODE`, so the command checks for `sh` with `Get-Command` and exits 1 when it is missing. Claude Code runs hooks with Git Bash on Windows and needs no `commandWindows`; without Git Bash it uses PowerShell, where this guard cannot run. Cursor and Gemini CLI ignore `commandWindows`. On Copilot, a hook with one plain command writes it as the `powershell` field; a portable `on:` hook, `args`, or a command list keeps one `command`, with a sync note. ### .agnostic-ai/scripts/no-force-push.sh ```sh #!/bin/sh # Blocks git push --force, -f, --mirror, or a +refspec, and lets # --force-with-lease through. It catches a mistake. It is not a sandbox. # Reads tool_input.command from the hook JSON on stdin and splits it into # words the way sh would: quotes, backslashes, line continuations, # comments, redirections, heredoc bodies, and $( ) or backtick # substitutions. It checks each command between unquoted ; & | ( ) and # newlines, past reserved words such as if and then. A heredoc it cannot # read ends the check, since a missed push beats blocking text. A coarse # pass over the unquoted text then blocks a force push the parse missed. awk -v q='"' -v sq="'" ' function flush() { if (inword && redirect) redirect = 0 else if (inword) { words[++n] = w quoted[n] = wq } w = "" inword = 0 wq = 0 } # Drops a file descriptor number written right before a redirection. function redirect_from() { if (inword && !wq && w ~ /^[0-9]+$/) { w = "" inword = 0 } flush() redirect = 1 } # Returns the index of the word that names the program, past reserved # words, assignments, and wrappers such as env and sudo. function program( i, a, wrapper) { for (i = 1; i <= n; i++) { a = words[i] if (!quoted[i] && a ~ /^(if|then|else|elif|do|while|until|!|time|\{)$/) continue if (a ~ /^[A-Za-z_][A-Za-z0-9_]*=/) continue if (a ~ /^(command|exec|env|nohup|nice|sudo|xargs)$/) { wrapper = a; continue } if (wrapper != "" && a ~ /^-/) { if ((wrapper == "env" && a ~ /^(-[uCS]|--(unset|chdir|split-string))$/) || (wrapper == "exec" && a == "-a") || (wrapper == "nice" && a ~ /^(-n|--adjustment)$/) || (wrapper == "xargs" && a ~ /^-[InLPsEda]$/) || (wrapper == "sudo" && a ~ /^(-[ugCDpUrtTR]|--(user|group|close-from|chdir|prompt|other-user|role|type|command-timeout|host))$/)) i++ continue } if (i > 1 && words[i - 1] == "time" && a == "-p") continue return i } return n + 1 } function check( i, j, k, a, plus, lease, positional) { i = program() if (i > n || words[i] !~ /(^|\/)git(\.exe)?$/) return for (i++; i <= n && words[i] ~ /^-/; i++) if (words[i] ~ /^(-C|-c|--git-dir|--work-tree|--namespace)$/) i++ if (i > n || words[i] != "push") return for (j = i + 1; j <= n; j++) { a = words[j] if (positional || a !~ /^-./) { if (a ~ /^\+/) plus = 1 continue } if (a == "--") { positional = 1; continue } if (a == "--force") { blocked = 1; return } if (a ~ /^--force-with-lease(=|$)/) { lease = 1; continue } if (a == "--mirror") { plus = 1; continue } if (a ~ /^--(repo|receive-pack|exec|push-option)$/) { j++; continue } if (a ~ /^--/) continue for (k = 2; k <= length(a); k++) { if (substr(a, k, 1) == "f") { blocked = 1; return } if (substr(a, k, 1) == "o") { if (k == length(a)) j++; break } } } if (plus && !lease) blocked = 1 } function end_command() { flush() check() n = 0 redirect = 0 } # Starts a subshell or substitution. A substitution keeps the words of the # command around it, which continue once it closes. function open_nested(kind, back, i) { depth++ kinds[depth] = kind backs[depth] = back if (kind == "(") { end_command(); return } saved_n[depth] = n for (i = 1; i <= n; i++) { saved_words[depth, i] = words[i] saved_quoted[depth, i] = quoted[i] } saved_w[depth] = w saved_wq[depth] = wq saved_redirect[depth] = redirect n = 0 w = "" inword = 0 wq = 0 redirect = 0 } function close_nested( i) { end_command() mode = backs[depth] if (kinds[depth] != "(") { n = saved_n[depth] for (i = 1; i <= n; i++) { words[i] = saved_words[depth, i] quoted[i] = saved_quoted[depth, i] } w = saved_w[depth] "$()" inword = 1 wq = saved_wq[depth] redirect = saved_redirect[depth] } depth-- } # Queues the delimiter of the heredoc whose word starts at p and returns # the position of its last character, or 0 for a form it cannot read. function heredoc_word(p, strip, word, c, closing) { strip = 0 if (substr(cmd, p, 1) == "-") { strip = 1; p++ } while (substr(cmd, p, 1) == " " || substr(cmd, p, 1) == "\t") p++ word = "" for (; p <= size; p++) { c = substr(cmd, p, 1) if (c == sq || c == q) { closing = index(substr(cmd, p + 1), c) if (!closing) return 0 word = word substr(cmd, p + 1, closing - 1) p += closing continue } if (c == "\\") { p++; word = word substr(cmd, p, 1); continue } if (index(" \t\n;&|()<>", c)) break word = word c } if (word == "") return 0 heredocs[++pending] = word strips[pending] = strip return p - 1 } # Skips the queued heredoc bodies that start after the newline at p and # returns the position of the newline that ends the last terminator. function skip_bodies(p, h, rest, end, line) { for (h = 1; h <= pending; h++) { do { if (p >= size) { pending = 0; return size } rest = substr(cmd, p + 1) end = index(rest, "\n") line = end ? substr(rest, 1, end - 1) : rest p = end ? p + end : size if (strips[h]) sub(/^\t+/, "", line) } while (line != heredocs[h]) } pending = 0 return p } # Backs up the parse with a coarse look at the unquoted text, so a wrapper # or syntax the parse does not follow still blocks: within a span between # separators, git, then push as its subcommand, then --force, -f, or a # --mirror or +refspec without --force-with-lease. function coarse( spans, count, s, nw, ws, i, j, a, plus, lease) { count = split(raw, spans, /[;&|\n]/) for (s = 1; s <= count; s++) { nw = split(spans[s], ws, /[ \t\r]+/) for (i = 1; i <= nw; i++) { if (ws[i] !~ /(^|\/)git(\.exe)?$/) continue for (j = i + 1; j <= nw && ws[j] ~ /^-/; j++) if (ws[j] ~ /^(-C|-c|--git-dir|--work-tree|--namespace)$/) j++ if (ws[j] != "push") continue plus = 0 lease = 0 for (j++; j <= nw; j++) { a = ws[j] if (a == "--force" || a ~ /^-[A-Za-z]*f[A-Za-z]*$/) return 1 if (a ~ /^--force-with-lease(=|$)/) lease = 1 else if (a ~ /^\+/ || a == "--mirror") plus = 1 } if (plus && !lease) return 1 } } return 0 } { json = json $0 "\n" } END { i = index(json, q "command" q) if (i == 0) exit 0 s = substr(json, i + 9) if (!match(s, /^[ \t\r\n]*:[ \t\r\n]*/)) exit 0 s = substr(s, RLENGTH + 1) if (substr(s, 1, 1) != q) exit 0 s = substr(s, 2) cmd = "" while (length(s) > 0) { c = substr(s, 1, 1) if (c == q) break if (c != "\\") { cmd = cmd c; s = substr(s, 2); continue } e = substr(s, 2, 1) if (e == "u") { code = tolower(substr(s, 3, 4)) if (code == "0026") cmd = cmd "&" else if (code == "003c") cmd = cmd "<" else if (code == "003e") cmd = cmd ">" else if (code == "0027") cmd = cmd sq else cmd = cmd " " s = substr(s, 7) continue } if (e == "n") cmd = cmd "\n" else if (e == "t") cmd = cmd "\t" else if (e == "r" || e == "b" || e == "f") cmd = cmd " " else cmd = cmd e s = substr(s, 3) } n = 0 w = "" inword = 0 mode = "" raw = "" size = length(cmd) for (p = 1; p <= size && !blocked; p++) { c = substr(cmd, p, 1) e = substr(cmd, p + 1, 1) if (mode == "single") { if (c == sq) mode = "" else w = w c continue } if (c == "\\") { p++ if (e == "\n") continue if (mode == "double" && e != q && e != "\\" && e != "$" && e != "`") w = w c w = w e wq = 1 inword = 1 if (mode == "") raw = raw " " continue } if (mode == "double") { if (c == q) mode = "" else if (c == "$" && e == "(") { p++; open_nested("$(", "double"); mode = "" } else if (c == "`") { open_nested("`", "double"); mode = "" } else w = w c continue } raw = raw " " if (c == q) { mode = "double"; inword = 1; wq = 1; continue } if (c == sq) { mode = "single"; inword = 1; wq = 1; continue } if (c == "#" && !inword) { while (p < size && substr(cmd, p + 1, 1) != "\n") p++ continue } if (c == "$" && e == "(" && substr(cmd, p + 2, 1) != "(") { p++; open_nested("$(", ""); continue } if ((c == "<" || c == ">") && e == "(") { p++; open_nested("$(", ""); continue } if (c == "`") { if (depth && kinds[depth] == "`") close_nested() else open_nested("`", "") continue } if (c == "(") { open_nested("(", ""); continue } if (c == ")") { if (depth) close_nested() else end_command() continue } if (c == "<" && substr(cmd, p, 3) == "<<<") { redirect_from(); p += 2; continue } if (c == "<" && e == "<") { redirect_from() redirect = 0 p = heredoc_word(p + 2) if (!p) { stopped = 1; break } continue } if (c == "&" && e == ">") { flush() redirect = 1 p += (substr(cmd, p + 2, 1) == ">") ? 2 : 1 continue } if (c == ">" || c == "<") { redirect_from() if ((c == ">" && (e == ">" || e == "&" || e == "|")) || (c == "<" && (e == "&" || e == ">"))) p++ continue } if (c == " " || c == "\t" || c == "\r") { flush(); continue } if (index(";&|\n", c)) { raw = raw c end_command() if (c == "\n" && pending) p = skip_bodies(p) continue } raw = substr(raw, 1, length(raw) - 1) c w = w c inword = 1 } if (!blocked && !stopped) end_command() if (!blocked && coarse()) blocked = 1 exit (blocked ? 3 : 0) }' # awk exits 2 on its own errors, so a block comes back as 3. case $? in 0) exit 0 ;; 3) echo "Blocked: git push --force rewrites the remote branch. Use --force-with-lease, or run the push yourself." >&2 exit 2 ;; *) exit 1 ;; esac ``` Check it before a session does: ```sh agnostic-ai hook run no-force-push --bash 'git push --force origin main' --expect block agnostic-ai hook run no-force-push --bash 'git status' --expect allow ``` A guard like this catches a mistake but is not a sandbox: a push from a script file or an alias gets through. To block a command for good, use [`permissions.deny`](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules) and each tool's own sandbox: [Claude Code](https://code.claude.com/docs/en/sandboxing), [Codex](https://learn.chatgpt.com/docs/sandboxing). `sync` copies the script, with its permissions and subdirectories under `scripts/`, into each tool's script directory and changes the command to run that copy. A missing script fails sync. Make the source executable when the command runs it directly. A file at `.agnostic-ai/scripts//guard.sh` replaces the shared script for that tool. For a shell command that starts an interpreter, quote the path: `command: 'node ".agnostic-ai/scripts/guard.mjs"'`. The path can also follow a [project-root variable](https://agnostic-ai.org/docs/spec-format/hook-reference/#imported-project-root-paths). Existing `.claude/hooks/`, `.codex/hooks/`, and `.gemini/hooks/` paths keep working. `sync --global` copies scripts from the global source root's `scripts/` directory into the selected tools' user hook directories, and its commands use absolute paths to those copies. [Copilot user hooks](https://agnostic-ai.org/docs/targets/copilot/#hooks) use the same command fields and event casing as project hooks; an imported one records its source file in `x-copilot.user-hook-file`. Script sharing does not translate event names or reply formats, so pick ones every tool supports. ### Where each tool keeps its scripts Most tools use `./hooks/`. The exceptions: | Target | Script directory | |--------|------------------| | Goose | its plugin's `hooks/` directory, following `outputs.goose.hooks-file` | | Windsurf | `.devin/hooks/` | | Antigravity | `.agents/hooks/` | | Zed | `.zed/hooks/`, when `outputs.zed.tasks-file` is set | | Cline | `.cline/hooks/scripts/`, apart from native hook definitions | | Copilot | `.github/hooks/scripts/`, apart from native hook definitions | | Kiro | `.kiro/scripts/`, outside its hook-definition directory | # MCP servers `mcps/` connects agents to outside tools and data through the Model Context Protocol: a browser, a database, GitHub, an issue tracker. Each tool keeps servers in its own file and format, such as `.mcp.json`, `.cursor/mcp.json`, or `[mcp_servers.*]` in `.codex/config.toml`. One YAML file per server feeds all of them. [`agnostic-ai compare claude cursor`](https://agnostic-ai.org/docs/cli-reference/inspect/#compare) reports the connection fields each tool gets and the references it leaves out, without printing values or connecting to servers. ## Write one `agnostic-ai new mcp filesystem` creates `mcps/filesystem.yaml`, plain YAML with no body. Replace its placeholder with: ```yaml name: filesystem description: Local filesystem access for the model. type: stdio command: npx args: - -y - "@modelcontextprotocol/server-filesystem" - /path/to/allowed/dir ``` Replace the path with a real directory, sync, and check that the server connects in your coding tool. A remote server for two tools, defined but switched off: ```yaml name: docs-search description: Search the internal docs. type: http url: https://mcp.example.com/mcp disabled: true targets: [claude, codex] ``` [MCP recipes](https://agnostic-ai.org/docs/spec-format/mcp-recipes/) has real servers to copy. ## Fields A server needs `command` (stdio) or `url` (remote). Only [`agnostic-ai lint`](https://agnostic-ai.org/docs/cli-reference/check/#lint) reports a missing one (LINT008), so run it before `sync`. | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | yes | none | Server identifier and key in the generated config, not the filename. Package-style slashes, such as `npm:@modelcontextprotocol/server-sequential.thinking`, are percent-encoded in filenames and kept in configs. On Windows, import refuses a server whose spec file name would hold a colon, which a name with a colon and no slash does; see [path semantics](https://agnostic-ai.org/docs/configuration/#path-semantics). | | `description` | no | empty | Free-form notes. Dropped for Junie and Warp. | | `type` | no | `stdio` | `stdio`, `http`, `sse`, or `ws`. Remote types write an explicit `type`; `stdio` does not. Augment, Factory, and Qoder get no server for `ws`, and Ellipsis none for `sse` or `ws`. | | `command` | stdio only | none | Executable to launch. | | `args` | no | empty | Argument list for the command. An element can hold a [reference](#references-in-url-and-args). | | `env` | no | empty | Environment variables for the server. Each value is a [reference](#environment-references) or a [plain setting marked `!literal`](#plain-settings). | | `url` | http/sse/ws only | none | Endpoint URL. It can hold a [reference](#references-in-url-and-args). | | `headers` | no | empty | HTTP headers for `http`/`sse`. Each value is a [reference](#environment-references) or a [plain setting marked `!literal`](#plain-settings). | | `cwd` | no | empty | Working directory for a stdio server, where the tool supports it. | | `timeout` | no | empty | Milliseconds on most tools, including [Junie](https://agnostic-ai.org/docs/targets/junie/#mcp). | | `oauth` | no | empty | OAuth settings. The format differs by tool. | | `disabled` | no | `false` | See [`disabled` support by target](#disabled-support-by-target). | | `roots` | no | empty | List of `{uri, name}` objects, for tools with MCP roots. | | `agent-only` | no | `false` | Keep the server out of every MCP file. Only the agents that name it can reach it. See [Agent-only servers](#agent-only). | ## Agent-only servers {#agent-only} Set `agent-only: true` to give a server to some agents and keep its tools out of the main session's context. ```yaml # mcps/github.yaml name: github agent-only: true command: npx args: ["-y", "@modelcontextprotocol/server-github"] env: GITHUB_TOKEN: ${GITHUB_TOKEN} ``` ```yaml # agents/triager.md frontmatter mcpServers: [github] ``` Sync leaves the server out of every project and user MCP file and writes its full definition into each agent file that names it, on Claude Code, Qoder, Kiro, and OpenHands. Every other tool gets no copy, with a note ([`mcpServers` support by target](https://agnostic-ai.org/docs/spec-format/agents/#mcpservers-support-by-target)). - An agent file is committed, so a server written there is checked more strictly: a credential in `url`, `args`, an `x-` block's `env` or `headers`, or a `${NAME:-default}` default is an error (LINT035), even from a [pack](https://agnostic-ai.org/docs/packs/), instead of the [LINT049 warning](#plain-settings). - Sync and `validate` refuse an `agent-only` value other than `true` or `false`, such as `yes`. - A server no agent names reaches no tool. Sync prints a note. - A `disabled: true` server is left out of the agent file. - `sync --global` writes no agent-only server. - Import does not create agent-only servers yet. `import claude` keeps an inline server in the agent's `mcpServers` list, and sync writes it back as it was. ## Secrets and plain settings {#plain-settings} A value in `env` or `headers` is a reference by default: `${NAME}`, `${NAME:-default}`, or `$${NAME}` text. A header may put one word before it, as in `Bearer ${API_KEY}`. Mark a plain setting that is not a secret with `!literal`: ```yaml env: GITHUB_TOKEN: ${GITHUB_TOKEN} NODE_ENV: !literal production ``` Sync writes `NODE_ENV: production` to every tool. An editor with the YAML language server needs `!literal scalar` in its `yaml.customTags` setting to accept the tag. Any other `env` or `headers` value, including text around a reference such as `postgres://u:pw@${HOST}/db`, is LINT035: `sync` refuses it unless the spec comes from a [pack](https://agnostic-ai.org/docs/packs/). The message names the server, field, and key, never the value. Empty values, numbers, and booleans are not checked. A credential in `url` or `args`, in an `x-` block's `env` or `headers`, or in a `${NAME:-default}` default gets a LINT049 warning, and `sync` still runs. The check reads the value's shape, since those places take no `!literal` mark. [`agnostic-ai migrate --only secrets`](https://agnostic-ai.org/docs/cli-reference/upgrades/#migrate) rewrites existing specs. A value that looks like a [credential](https://agnostic-ai.org/docs/spec-format/mcp-import/#what-import-writes) becomes a reference, such as `GITHUB_TOKEN: ${GITHUB_TOKEN}`, and the command lists each variable to set, never the value. Every other value gets `!literal`. It leaves two cases to you: a key with a [credential name](https://agnostic-ai.org/docs/spec-format/mcp-import/#credential-names) whose value does not look like a credential, such as `API_KEY: sk-live-abc`, and a credential around a reference. ## Environment references Write a token or key as `${NAME}`, whole or in part, in an `env` or `headers` value, a `url`, or an `args` element: ```yaml env: GITHUB_TOKEN: ${GITHUB_TOKEN} headers: Authorization: Bearer ${API_KEY} url: https://${API_HOST}/mcp args: [--token, "${GH_TOKEN}"] ``` `${NAME:-default}` uses `default` when `NAME` is unset. Only Claude Code, Crush, OpenHands, and Gemini support that form. Any other `${...}`, such as `${env:NAME}` or `${input:id}`, is not a reference. Sync writes each tool's own form, and never writes a reference as plain text. Where a tool cannot read a reference in that field, including a `${NAME:-default}` outside the four tools above, the note names the server, the key, and the variable, and: - in `env` or `headers`, sync leaves the key out, as it does for any other `${...}` there; - in `url` or `args`, sync leaves the whole server out, because dropping one argument would change the command. | Target | `env` and `headers` | `url` and `args` | |--------|---------------------|------------------| | [Claude Code](https://agnostic-ai.org/docs/targets/claude/), [Crush](https://agnostic-ai.org/docs/targets/crush/), [OpenHands](https://agnostic-ai.org/docs/targets/openhands/), [Gemini](https://agnostic-ai.org/docs/targets/gemini/) | `${NAME}`, `${NAME:-default}` | `${NAME}`, `${NAME:-default}` | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/), [Windsurf](https://agnostic-ai.org/docs/targets/windsurf/) | `${env:NAME}` | `${env:NAME}` | | [OpenCode](https://agnostic-ai.org/docs/targets/opencode/) | `{env:NAME}` | `{env:NAME}` | | [Continue](https://agnostic-ai.org/docs/targets/continue/) | `{% raw %}${{ secrets.NAME }}{% endraw %}`, in `env` on stdio servers and under `requestOptions.headers` | `{% raw %}${{ secrets.NAME }}{% endraw %}` | | [Amp](https://agnostic-ai.org/docs/targets/amp/) | `headers`: `${NAME}`. `env`: key left out | `url`: `${NAME}`. `args`: server left out | | [Factory](https://agnostic-ai.org/docs/targets/factory/), [Kiro](https://agnostic-ai.org/docs/targets/kiro/), [Ellipsis](https://agnostic-ai.org/docs/targets/ellipsis/) | `${NAME}` | Server left out | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | `env`: `KEY: ${KEY}` adds `KEY` to `env_vars`. `headers`: `Authorization: Bearer ${NAME}` sets `bearer_token_env_var`, and another header of exactly `${NAME}` joins `env_http_headers`. Any other reference: key left out | Server left out | | [Antigravity](https://agnostic-ai.org/docs/targets/antigravity/), [Augment](https://agnostic-ai.org/docs/targets/augment/), [Copilot](https://agnostic-ai.org/docs/targets/copilot/), [Junie](https://agnostic-ai.org/docs/targets/junie/), [Kilo Code](https://agnostic-ai.org/docs/targets/kilo/), [Qoder](https://agnostic-ai.org/docs/targets/qoder/), [Trae](https://agnostic-ai.org/docs/targets/trae/), [Warp](https://agnostic-ai.org/docs/targets/warp/), [Zed](https://agnostic-ai.org/docs/targets/zed/) | Key left out | Server left out | Kiro expands only variables approved under **Mcp Approved Env Vars** in its settings. Factory fails the connection when a referenced variable is unset. Continue's IDE extensions read secrets from project `.env`, `.continue/.env`, or `~/.continue/.env`, and its CLI also reads environment variables ([secret resolution](https://docs.continue.dev/faqs#managing-local-secrets-and-environment-variables)). Keep `.env` files out of Git. [`agnostic-ai doctor`](https://agnostic-ai.org/docs/cli-reference/check/#doctor) lists each referenced variable unset in your shell, including in `x-` blocks and disabled servers. A tool started from a desktop launcher may see a different environment. ### References in `url` and `args` `url` and `args` take the same references, with the stricter rule above: a tool that cannot read one there gets no server. - A tool's own form at the top level, such as `${env:NAME}`, is copied as text to every other tool, and `lint` reports it as LINT028. Write `${NAME}`, or put the tool's own text under `x-:`. - `${workspaceFolder}`, `${workspaceFolderBasename}`, `${userHome}`, `${pathSeparator}`, and any other `${...}`, such as `${input:id}`, are not references and stay as written. ### Literal `${NAME}` text {#literal-text} Some servers expand `${NAME}` themselves, such as `mcp-remote` in a header argument. Write `$${NAME}` to pass the text on unchanged. It works in `url`, `args`, `env`, and `headers`: ```yaml command: npx args: ["-y", "mcp-remote", "https://mcp.example.com", "--header", "x-chroma-token: $${X_CHROMA_TOKEN}"] ``` Sync writes `${X_CHROMA_TOKEN}` to every built-in tool, with no note; an external adapter gets the spec as written. Quote it inside a `[...]` list, as you would `${NAME}`. A tool whose column in the [table above](#environment-references) shows `${NAME}` for that field expands the text itself, from its own environment rather than the server's `env` block, before the server sees it. None of them can write a literal `${`, so sync still writes the server. A value expanded in `args` shows in the process list. When the variable is unset and has no default, Claude Code keeps the `${NAME}` text and warns in `claude mcp list`. ### What import writes `import` turns native references and literal credentials into `${NAME}` references, and lists each variable to set. [MCP import credentials](https://agnostic-ai.org/docs/spec-format/mcp-import/) owns the rewrite rules, [credential names](https://agnostic-ai.org/docs/spec-format/mcp-import/#credential-names), and detection limits. ## Target-only fields | Target | Extra fields | |--------|--------------| | [Codex](https://agnostic-ai.org/docs/targets/codex/) | `env_vars`, `env_http_headers`, `http_headers_helper`, `auth`, `required`, `startup_timeout_sec`, `startup_timeout_ms`, `tool_timeout_sec`, `default_tools_approval_mode`, `scopes`, `oauth_resource`, `experimental_environment`, `enabled_tools`, `disabled_tools` | | [Crush](https://agnostic-ai.org/docs/targets/crush/) | `enabled_tools`, `disabled_tools`, `sessionless`, `channel_enabled`, `channel_reply` | | [Gemini](https://agnostic-ai.org/docs/targets/gemini/) | `trust`, `includeTools`, `excludeTools` | | [Qoder](https://agnostic-ai.org/docs/targets/qoder/) | `trust`, `includeTools`, `excludeTools`, `alwaysAllow` | | [Kiro](https://agnostic-ai.org/docs/targets/kiro/) | `autoApprove`, `disabledTools`, `oauthScopes` | | [Factory](https://agnostic-ai.org/docs/targets/factory/) | `x-factory.enabledTools`, `disabledTools`, `connectTimeout` | | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | `alwaysLoad`, `bareElicitationCapability`, `headersHelper` | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/) | `envFile`, `auth` | | [Copilot / VS Code](https://agnostic-ai.org/docs/targets/copilot/) | `envFile`, `dev`, `sandboxEnabled` and `sandbox` (VS Code file only), `tools` (Copilot CLI files only) | | [Continue](https://agnostic-ai.org/docs/targets/continue/) | `connectionTimeout`, `requestOptions` | | [OpenHands](https://agnostic-ai.org/docs/targets/openhands/) | `auth: oauth`, or a truthy `oauth`, which sets `auth: "oauth"` on a remote server in an agent's `mcp_servers` | The Crush and Claude Code fields also work under `x-crush` or `x-claude`, which wins, and other tools leave them out. On Amp, set `x-amp.includeTools`. `x-factory`, `x-kilo`, and `x-continue` override the matching top-level options for that target. Each target page gives the details: [Factory tool lists](https://agnostic-ai.org/docs/targets/factory/#mcp), [Crush channels](https://agnostic-ai.org/docs/targets/crush/#mcp), and [Claude Code flags](https://agnostic-ai.org/docs/targets/claude-reference/#mcp-mappings). ## `disabled` support by target This table covers project files. For user files written by `sync --global`, see [global MCP servers](https://agnostic-ai.org/docs/global-configuration/#global-mcp-servers). | Target | Behavior | |--------|----------| | [Antigravity](https://agnostic-ai.org/docs/targets/antigravity/), [Crush](https://agnostic-ai.org/docs/targets/crush/), [Factory](https://agnostic-ai.org/docs/targets/factory/), [Kiro](https://agnostic-ai.org/docs/targets/kiro/), [Qoder](https://agnostic-ai.org/docs/targets/qoder/), [Windsurf](https://agnostic-ai.org/docs/targets/windsurf/) | Native `disabled` | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | Mapped to `enabled = false` | | [Kilo Code](https://agnostic-ai.org/docs/targets/kilo/), [OpenCode](https://agnostic-ai.org/docs/targets/opencode/), [Zed](https://agnostic-ai.org/docs/targets/zed/) | Mapped to `"enabled": false` | | [Copilot](https://agnostic-ai.org/docs/targets/copilot/) | A `disabledMcpServers` entry in `.github/copilot/settings.json` for Copilot CLI. The key is removed from both MCP files with a note. Disable the server in VS Code yourself | | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | Mapped to project `disabledMcpjsonServers` in `.claude/settings.json` for servers written to `.mcp.json`. Import restores it | | [Cursor](https://agnostic-ai.org/docs/targets/cursor/), [Augment](https://agnostic-ai.org/docs/targets/augment/), [Junie](https://agnostic-ai.org/docs/targets/junie/), [Trae](https://agnostic-ai.org/docs/targets/trae/), [Warp](https://agnostic-ai.org/docs/targets/warp/) | The key is removed with a note, and the server stays in the file. Disable the server in the tool itself; Warp starts a project server only when you start it | | [Ellipsis](https://agnostic-ai.org/docs/targets/ellipsis/) | Server left out, since a listed server is always available. Written into the environment file, not an MCP file | # MCP recipes Copy a recipe into `.agnostic-ai/mcps/.yaml` (or the directory `sources.mcps` sets). Replace example paths, and export each `${NAME}` variable before you start your coding tool. Run `agnostic-ai lint` and `agnostic-ai sync`, then confirm the server connects in the tool. The recipes were checked on 2026-10-03; compare each with its linked source. [MCP servers](https://agnostic-ai.org/docs/spec-format/mcps/) lists the fields, and [environment references](https://agnostic-ai.org/docs/spec-format/mcps/#environment-references) the values each tool cannot read. ## GitHub (remote) Source: [github/github-mcp-server, remote server](https://github.com/github/github-mcp-server/blob/main/docs/remote-server.md) and its [README](https://github.com/github/github-mcp-server#readme). ```yaml name: github description: GitHub issues, pull requests, and repositories. type: http url: https://api.githubcopilot.com/mcp/ headers: Authorization: Bearer ${GITHUB_PAT} ``` Create a personal access token on GitHub and export it as `GITHUB_PAT`. ### Keep it personal A shared spec uses each person's own `${GITHUB_PAT}` without storing the token. If only you want the server, save the spec as `.agnostic-ai/local/mcps/github.yaml`, whatever `sources.mcps` says. [Local files](https://agnostic-ai.org/docs/local-overrides/) stay out of version control, so teammates who want it add their own. A local file still loads, so it does not turn the server off. To keep a server off, leave it out of every spec that loads, or turn it off in the tool; `disabled: true` does not work on every tool ([`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target)). ## Context7 (remote) Source: [upstash/context7](https://github.com/upstash/context7#readme). ```yaml name: context7 description: Current library documentation for the model. type: http url: https://mcp.context7.com/mcp headers: Authorization: Bearer ${CONTEXT7_API_KEY} ``` A free key from the Context7 dashboard raises the rate limit. ## Playwright Source: [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp#readme). ```yaml name: playwright description: Drive a browser with Playwright. command: npx args: - "@playwright/mcp@latest" ``` ## Filesystem Source: [modelcontextprotocol/servers, filesystem](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem). ```yaml name: filesystem description: Read and write files in the listed directories. command: npx args: - -y - "@modelcontextprotocol/server-filesystem" - /path/to/allowed/dir ``` Each path after the package name is a directory the server may reach. # MCP import credentials The [Tools page's **Make it a spec** action](https://agnostic-ai.org/docs/cli-reference/inspect/#serve) uses these rewrite and refusal rules for one server, including on the global page. ## What import writes `import` replaces every literal `env` and `headers` value with a reference, because it cannot tell a token from a plain setting and a spec is meant to be committed. So `NODE_ENV: production` becomes `NODE_ENV: ${NODE_ENV}`, a variable you must now set; mark it [`!literal`](https://agnostic-ai.org/docs/spec-format/mcps/#plain-settings) by hand to put it back. Import prints each replacement and the variable to set. It reads each tool's own reference form in `env`, `headers`, `url`, and `args` back as `${NAME}`, including a whole-value `$NAME` on Gemini and Crush and `%NAME%` on Gemini. A `${NAME}` in a field the tool never expands, as in Warp, Zed, and Codex `args`, is plain text, so the spec gets [`$${NAME}`](https://agnostic-ai.org/docs/spec-format/mcps/#literal-text). - An `env` value reads the variable its key names. - A header reads `_
` in upper case, with any character other than a letter or digit as `_`, and keeps a `Bearer ` prefix outside the reference. - When two different values would share a name, each reads `_` instead, then `_2`, `_3`. Codex forwards a variable only under its key's own name, so a renamed `env` value such as `API_KEY: ${GH_API_KEY}` is left out of `.codex/config.toml` with a note; rename the variable to match the key when Codex needs that server. - A `${NAME:-default}` loses its default, since a default is a value too. - A value with text around its references, such as `postgres://u:pw@${HOST}/db` or `Bearer sk-1 ${EXTRA}`, is replaced whole, since that text may be the secret, and so is a value with any other `${...}`, such as a VS Code `${input:id}` prompt. - A Crush `$(command)` value becomes a reference. - When a `url` or argument is exactly one URL (no spaces, quotes, or `$${`, starting with a scheme or a `${NAME}`), only its credentials become references, each named `_`: a `user:password@` password, a username that is a token by the prefixes below (`https://ghp_...@github.com`), and a query or fragment parameter with a [credential name](#credential-names), such as `api_key`, `access_token`, or `clientSecret` (not `token_type`, `key_id`, or `sortKey`). A parameter named exactly `auth`, `sig`, `signature`, `code`, `session`, `jwt`, or `bearer` counts too (not `country_code`), and one named `auth` or `code` always becomes a reference, even without a secret. So `postgresql://admin:pw@db/app` in server `pg` becomes `postgresql://admin:${PG_PASSWORD}@db/app`. The rest of the URL stays as written, including a leading reference such as `${API_BASE}/mcp?token=...`, and the output never prints the URL. Import never rewrites another value, such as a `sh -c` command or `--url=...`. - In `args`, the value of a long flag with a [credential name](#credential-names), as `--api-key value` or `--api-key=value`, becomes a reference named after the flag: in server `gh`, `--api-key X` becomes `--api-key ${GH_API_KEY}`. `--token-file`, `--key-id`, `--sort-key`, and `--no-token` are not credentials. These values stay as written: a `$NAME`, a file path (starting with `/`, `~`, `./`, `../`, or a drive such as `C:\`), a file name ending in `.json`, `.txt`, `.pem`, `.key`, `.env`, `.yaml`, `.yml`, `.toml`, `.crt`, `.p12`, or `.db`, a value starting with `@`, a number, a boolean word, and lower-case words joined by dashes such as `streamable-http`. After a flag whose name ends in `key`, `apikey`, `authentication`, or `authorization`, a value needs at least 8 characters, so `--require-api-key run` stays as written. - An argument that is a whole token by its prefix becomes `${_TOKEN}`, then `_2`, `_3`. The prefixes are `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_`, `glpat-`, `xoxb-`, `xoxp-`, `xoxa-`, `xapp-`, `sk_live_`, `sk_test_`, `rk_live_`, `sk-proj-`, `sk-ant-`, `npm_`, `AIza`, `sk-`, and `AKIA`, each with a plausible length. A JWT (`eyJ...` with three dot-separated parts) counts too. - When a value it does not rewrite still holds a literal credential, import leaves the server out and names the server and field. A credential here is any of these: - a URL password or credential parameter (a quoted value such as `token='...'` counts, a quoted `'${NAME}'` does not); - a token by its prefix inside a longer value; - a `Bearer` with a literal token that has a digit, one of `-._~+/=`, or at least 20 characters; - a line `Name: value` or a JSON pair `"name": "value"` with a credential key and a literal value, as in a headers value `X-Api-Key: ...` or `{"apiKey": "..."}`; - a `--header` or `-H` argument such as `Authorization: ...`, `Cookie: ...`, or `X-Api-Key: ...` with a literal value; - a `user:password@host` without a scheme, as in `root:pw@db:3306`; - a `NAME=value` or `--name value` with a credential name inside a longer value, as in `sh -c "API_TOKEN=... exec srv"` or `sh -c "npx srv --token ..."`; - an all-digit password before `#`, or before `/` with no host, as in `redis://:1234#5@host`. - Import never rewrites `command`, `cwd`, Continue `requestOptions` keys other than `headers`, or an `x-` block. A credential in any of them, or a key there with a credential name and a literal value, leaves the server out. - Import does not find these credentials, so check such servers by hand: - a short flag such as `-p secret` or `-psecret`, since `-p` may be a port or profile, or a single-dash flag such as `-token secret`; - a credential value that starts with `-` in its own argument after a flag; - a value shorter than 8 characters in its own argument after a flag whose name ends in `key`, `apikey`, `authentication`, or `authorization`; - a `Bearer` token shorter than 20 characters with only letters; - a token in a URL path, such as a Slack webhook `https://hooks.slack.com/services/...`, also under `import --global`; - a URL username that is a token without one of the prefixes above; - a token format not listed above. `import --global` cannot change a server, so it leaves out each server with a literal credential: any credential above, or an `env` or header value with a [credential name](#credential-names) or that looks like a credential. It names the server and field, never the value. A plain setting such as `NODE_ENV: production`, a path, a number, or `Bearer ${TOKEN}` imports as written, marked [`!literal`](https://agnostic-ai.org/docs/spec-format/mcps/#plain-settings). A value under a credential name that does not look like a credential stays unmarked, so `lint` asks about it. Add a left-out server by hand under `local/mcps`, or with a `${NAME}` reference. ## Setting values Imports of other settings use one rule for each value: Claude Code `env` values, hook headers, and `mcp_tool` inputs, Codex overlay values, and Grok hook and agent metadata. A value holds a credential when it has literal text and either the text looks like a credential by the rules above, or its key has a [credential name](#credential-names) and the value is not a boolean (`true`, `false`, `yes`, `no`, `on`, `off`, `0`, `1`), a path, or a file name. - Literal text is anything other than `${NAME}` and `$NAME` references, with an optional `Bearer`, `Basic`, or `Token` before them. Under a key that ends in `authorization`, such as `Proxy-Authorization`, any one word may come first, as in `SSWS ${OKTA_TOKEN}`. A `${NAME:-default}` with a default and an escaped `$${NAME}` count as literal. - A path starts with `~/`, `./`, `../`, a drive such as `C:\`, or `/` with at least two segments, such as `/etc/x`. A value that reads as base64 with slashes, such as `/9j/4AAQSkZJRg...`, is not a path: a segment of 16 or more letters and digits with a digit, or 24 or more in both letter cases, or 20 or more letters and digits in all, in both letter cases, with no `.`, `-`, `_`, or `~`. A file name has no path and ends in an extension listed above. Neither may hold a token. So `API_TOKEN: 12345678`, `API_TOKEN: hunter $X`, `API_TOKEN: ${API_TOKEN:-x}`, `PASSWORD: ~hunter2`, and `Authorization: Bearer abc` hold one. `API_TOKEN: ${API_TOKEN}`, `API_TOKEN: $A $B`, `Authorization: ApiKey ${ES_KEY}`, `CLAUDE_CODE_SKIP_BEDROCK_AUTH: 1`, `GOOGLE_APPLICATION_CREDENTIALS: /path/sa.json`, and `CLIENT_KEY: client.key` do not. Claude Code import leaves out such an `env` value with a warning, and the whole matcher group of a hook with such a header or input. It also leaves out any literal value of its fixed token variables, such as `ANTHROPIC_API_KEY`, and header variables, such as `OTEL_EXPORTER_OTLP_HEADERS` (see [overlays](https://agnostic-ai.org/docs/spec-format/overlays/)). Codex import leaves out such a value with a warning; keys that start with `env_` and prose keys such as `instructions` use their own checks, as the [Codex reference](https://agnostic-ai.org/docs/targets/codex-reference/) says. Grok import stops before writing. ## Credential names A name is a credential name when its last word is `token`, `secret`, `password`, `passwd`, `passphrase`, `pwd`, `pass`, `accesstoken`, `authtoken`, `credential`, `credentials`, `cookie`, `bearer`, `apikey`, `authentication`, or `authorization`. Words split at punctuation and camelCase, so `clientSecret` and `API_TOKEN` count and `token_type` does not; `pass_phrase` counts as `passphrase`. A one-word name ending in `password`, `passwd`, `passphrase`, `secret`, or `token` counts too, as `PGPASSWORD` and `MYTOKEN` do. A last word `key` counts unless the word before it is `sort`, `cache`, `public`, `partition`, `primary`, `foreign`, `idempotency`, `routing`, `object`, `row`, `hash`, `map`, `lookup`, `group`, `dedup`, `shard`, `index`, `unique`, `composite`, `natural`, `surrogate`, `ssh`, `gpg`, `pgp`, `s3`, or `id`. So `api_key`, `OPENAI_KEY`, and `--openai-key` count, and `sort_key`, `cache_key`, `--public-key`, and `--ssh-key` do not. In an `env` or header key, an `x-` or `requestOptions` key, and a `Name: value` line or JSON pair, a last word `cred` or `auth` counts too, as in `MY_CRED` and `X_AUTH`. A flag such as `--auth oauth` does not. # Settings `settings/` sets what agents may run without asking, what they must never run, and which model and effort they start with. Claude Code keeps this in `.claude/settings.json`, Factory in command lists, Codex in `config.toml`. A settings spec states it once. ## Write one `agnostic-ai new settings project-defaults` creates a template with no permissions or model. Settings specs are plain YAML. Save this as `.agnostic-ai/settings/project-defaults.yaml`: ```yaml permissions: allow: - shell(go test:*) model: claude: sonnet codex: sol ``` Claude Code writes the permission into `.claude/settings.json`. Codex writes the model, but writes shell rules only with `outputs.codex.exec-policies-from-permissions: true`; review the [translation limits](https://agnostic-ai.org/docs/targets/codex-reference/#translate-bash-permissions) first. Other tools are listed [below](#support-by-target). Run `agnostic-ai explain settings/project-defaults.yaml`, then sync. `explain` and `compare` show the written policy, not what the tool enforces, so confirm approval behavior in your coding tool. ## Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `permissions.allow` | no | empty | Rules approved without prompting. | | `permissions.deny` | no | empty | Rules always blocked. | | `permissions.ask` | no | empty | Rules that prompt before running. | | `permissions.default-mode` | no | unset | Claude Code's starting mode for `sync --global`: `default`, `manual`, `acceptEdits`, `plan`, `auto`, `dontAsk`, or `bypassPermissions`. Other tools raise a sync note. | | `model` | no | empty | Default model: a string, a map per target with an optional `default` (like [agent `model`](https://agnostic-ai.org/docs/spec-format/agents/#per-target-model-and-effort)), or a [tier](https://agnostic-ai.org/docs/spec-format/agents/#model-tiers) name. A target with no entry and no `default` gets no model. | | `effort` | no | empty | Default reasoning effort: a single value, or a map per target with an optional `default`. An agent's own `effort` is separate. See [effort by target](#effort-by-target). | | `session-retention-days` | no | unset | Personal session age limit, an integer of 1 or more. Only `sync --global` writes it. See [session retention](#session-retention). | | `voice` | no | unset | Personal voice dictation: `enabled`, `mode` (`hold` or `tap`), and `language`. Only `sync --global` writes it. See [voice dictation](#voice-dictation). | | `protected` | no | unset | Files agents must not edit without asking. See [protected paths](#protected-paths). | ## Session retention Keep this personal setting under `~/.agnostic-ai/settings/`, outside team project specs: ```yaml # ~/.agnostic-ai/settings/retention.yaml session-retention-days: 3650 ``` `sync --global` writes the tool's own age limit; unset leaves it alone. Values that are not an integer of 1 or more fail validation (LINT045). Project sync only prints a note. | Tool | User settings file | Native key | |------|--------------------|------------| | Claude Code | `~/.claude/settings.json` | `cleanupPeriodDays: 3650` | | CodeBuddy Code | `~/.codebuddy/settings.json` | `cleanupPeriodDays: 3650` | | Gemini CLI | `~/.gemini/settings.json` | `general.sessionRetention.maxAge: "3650d"` | | Qoder CLI | `~/.qoder/settings.json` | `general.sessionRetention.maxAge: "3650d"` | - An `x-claude.cleanupPeriodDays` or `x-codebuddy.cleanupPeriodDays` value wins for its target. - An existing invalid Claude value, including `cleanupPeriodDays: 0`, can pause cleanup. Sync leaves it unchanged, even with `--backup`, and asks you to fix it by hand or set `x-claude.cleanupPeriodDays`. - If Gemini or Qoder has `general.sessionRetention.enabled: false`, sync leaves its age limit alone and prints a note. It never sets the cleanup switch or `maxCount`, and Gemini's existing `maxCount` can still remove younger sessions. - Other targets print a note. Kilo's deletion switch and Factory's organization setting are outside this field. - Lowering the limit warns that older sessions will be deleted, including when removing the field restores a shorter native limit. Pass `--backup` to replace a native value set outside agnostic-ai. - Removing the field restores the original value's bytes, even one that already matched, and removes an empty file that sync created. A value changed by hand stays yours. This restore data needs ownership state version 7 or later. Global sync now writes version 10; older releases refuse that state rather than discard it. - `explain --global settings/retention.yaml` lists the mapped keys. agnostic-ai changes settings only; it never reads, copies, or archives transcripts. Claude Code also applies this cutoff to some other session data and orphaned worktrees, and Desktop and Cowork transcripts have a separate setting. See [Claude cleanup](https://code.claude.com/docs/en/claude-directory), [Gemini configuration](https://geminicli.com/docs/reference/configuration/), and [Qoder settings](https://docs.qoder.com/cli/settings-reference). ## Voice dictation Keep this personal setting under `~/.agnostic-ai/settings/`, outside team project specs: ```yaml # ~/.agnostic-ai/settings/voice.yaml voice: enabled: true mode: tap language: es ``` `sync --global` writes each member the tool has a key for; an unset member leaves the tool's value alone. `mode` is `hold` (hold a key while you talk) or `tap` (tap to start and stop). `language` is passed as written. Other values fail validation (LINT054). Project sync only prints a note. | Tool | User settings file | Native keys | |------|--------------------|-------------| | Claude Code | `~/.claude/settings.json` | `voice.enabled`, `voice.mode` | | Qwen Code | `~/.qwen/settings.json` | `general.voice.enabled`, `general.voice.mode`, `general.voice.language` | | Gemini CLI | `~/.gemini/settings.json` | `experimental.voiceMode`, `experimental.voice.activationMode` (`hold` becomes `push-to-talk`, `tap` becomes `toggle`) | | Grok Build | `~/.grok/config.toml` | `ui.voice_capture_mode` (`tap` becomes `toggle`), `ui.voice_stt_language` | | Mistral Vibe | `~/.vibe/config.toml` | `voice_mode_enabled` | | Kiro CLI | `~/.kiro/settings/cli.json` | Literal root key `voice.language` | | Warp | [Platform settings file](https://agnostic-ai.org/docs/targets/warp/#voice-dictation) | `agents.voice.voice_input_enabled`, `agents.voice.voice_input_language` | - Claude Code takes its dictation language from its response language, so sync does not write `voice.language` there and prints a note. Set `x-claude.language` to change both. - Qwen keeps dictation off until a voice model is chosen. Run `/model --voice` or set `x-qwen.voiceModel`; sync prints a note when no model is configured. - Gemini's voice keys and Vibe's voice mode are experimental. Gemini has no language key; Vibe has no mode or language key. Sync prints a note for each member without a native key. - Grok has no dictation on/off key: `/voice` always works. Set `x-grok.ui.voice_keybind_enabled` to control its shortcut separately. `GROK_HOME` moves its user configuration directory. - Kiro CLI maps language only; `auto` detects it. Its on/off and capture mode members print a note. `KIRO_HOME` moves the global directory. Native `x-kiro` keys use literal dotted names, such as `voice.language`. - Other targets print a note naming their limit. - An `x-` key wins over the portable member it overlaps, including a parent map. Set keys such as `x-claude.voice.autoSubmit` there. - Pass `--backup` to replace a native value set outside agnostic-ai. - Removing the field restores each key's original bytes, removes an empty `voice` object that sync created, and removes an empty file that sync created. A value changed by hand stays yours. - `explain --global settings/voice.yaml` lists the mapped keys. `import --global` reads these keys into one shared `voice` map. Tool arguments select the native files to read, so importing Claude alone can supply voice settings to Qwen on a later sync. A tool whose home spec already sets `voice` is skipped. Import excludes those tools with `targets-exclude` to keep their existing values; model and effort values for excluded tools go into `settings/imported-models.yaml`. An earlier model import can receive a missing `voice` field without changing its existing values or comments. Import uses `settings/imported-voice.yaml` when scope, formatting, or other incoming settings require a separate file. If that name is already in use by either layer, it selects a numbered name. Values from the first tool named win when tools disagree; import prints the values it skipped. Claude's response language stays out of `voice.language`. Its older `voiceEnabled` key is read when the `voice` object is absent; sync reuses a matching legacy value without adding a nested key. See [Claude voice dictation](https://code.claude.com/docs/en/voice-dictation), [Qwen settings](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/), [Gemini's voice schema](https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/config/settingsSchema.ts), [Grok settings](https://docs.x.ai/build/settings/reference), and [Vibe voice mode](https://github.com/mistralai/mistral-vibe#voice-mode). ## Protected paths A `protected` block states "do not edit these files without asking" once, for CI workflows, lock files, generated code, or migrations. Sync writes the strongest guard each tool has. ```yaml protected: paths: - .github/** - composer.lock decision: ask # ask (default) or deny reason: CI and the lock file change only on purpose. ``` | Field | Required | Default | Description | |-------|----------|---------|-------------| | `paths` | yes | | Globs from the project root, so `composer.lock` protects only the root file and `**/composer.lock` every copy. `*` and `?` match inside one path segment, `**` across segments. A path covers every file under it (`vendor` protects `vendor/a.go`); a trailing `/` means the directory's contents. | | `decision` | no | `ask` | `ask` makes the agent ask first. `deny` blocks the edit. | | `reason` | no | empty | Shown to the agent when it is blocked from an edit. | - Character classes, braces, negation, and paths outside the project are rejected, because tools would read them differently. - Each settings spec holds one block, so use one file per decision. - The Codex and Gemini CLI hooks and `lint` ignore case, so `.GITHUB/ci.yml` counts as protected. Claude Code's docs do not say whether `Edit` rules ignore case. | Target | Protection | How | |---|---|---| | Claude Code | enforced (permission) | `Edit(/)` rules in `permissions.ask` or `permissions.deny` ([details](https://agnostic-ai.org/docs/targets/claude/#protected-paths)) | | Codex | enforced (hook) | a generated `PreToolUse` hook in `.codex/hooks/` that blocks a matching `apply_patch` ([details](https://agnostic-ai.org/docs/targets/codex/#protected-paths)) | | Cursor | enforced (permission), CLI only, `deny` only | `Write()` rules in `permissions.deny` of `.cursor/cli.json`; `ask` is not enforced ([details](https://agnostic-ai.org/docs/targets/cursor/#protected-paths)) | | Gemini CLI | enforced (hook) | a generated `BeforeTool` hook in `.gemini/hooks/` that blocks a matching `write_file` or `replace` ([details](https://agnostic-ai.org/docs/targets/gemini/#protected-paths)) | | Every other target | not enforced | a sync note. State the paths in a rule | Protection covers the agent's edit tools; a shell command can still change the file. `lint` warns (LINT022) when a protected path covers a file sync writes, since sync rewrites it, and reports an invalid block as LINT023. `sync --global` does not write protected paths. ## Permission rules A rule uses the [capability](https://agnostic-ai.org/docs/spec-format/agents/#capabilities) names an agent's `can:` takes. Unlike an agent's tool list, a permission rule keeps its scope on Claude Code: `read(src/**)` becomes `Read(src/**)`, `edit(src/**)` becomes `Edit(src/**)`, `shell(git status)` becomes `Bash(git status)`, and `mcp:github/get_issue` becomes `mcp__github__get_issue`. - Each rule becomes the tool's own permission name. `delete` raises a sync note where a tool has no delete permission. - A Claude Code rule still works as an alias, such as `WebFetch(domain:go.dev)`, and a list can mix both. A Claude Code rule is a bare tool name (the whole tool) or `Scope(argument)`. `Scope()` with an empty argument is dropped, not read as the bare tool, which would widen it. - `write` takes no path. Claude Code checks file writes against `Edit` rules only, so write `edit()` to cover a file; it also covers writes. - `web` has no scoped form. To limit web access, use the tool's own permission fields, or the `WebFetch(domain:example.com)` alias. - `validate`, `lint` (LINT036), and `sync` stop on a rule they cannot read, including one in a pack and an unquoted rule that YAML reads as a mapping, since a tool would drop it. - `agnostic-ai migrate --only capabilities` rewrites each Claude Code rule that has a capability of its own, and an adjacent `WebFetch, WebSearch` pair as `web`. The rest stay as aliases, and sync writes the same files. - `explain settings/.yaml` lists the rules each tool gets and any extra access. `on-unsupported: error` fails when a rule widens, such as a Codex exact shell allow that becomes a command prefix. A bare capability under `allow` or `ask` covers the whole tool. Older releases matched these lowercase rules to no tool, so an old spec now grants more: `lint` warns (LINT038, which `lint --strict` fails on) and `sync` prints a note once per run, naming the affected permissions. `on-unsupported: silent` or `sync --quiet` hides the note. Scoped rules, `deny` rules, tool-specific overrides that replace the rule, and global lists (written only for Claude Code) get no warning. Keep a `shell` wildcard at the end of an `allow` or `deny` rule. `shell(git * main)` also approves options placed at the `*`, and Claude Code matches a mid-command `*` in a `deny` rule literally, so it blocks nothing. `lint` reports both as LINT009. ## Merging Several files merge. Permission lists are joined in source order with duplicates removed. The last `model` and `effort` set wins, per target, so `model: {codex: gpt-6-luna}` in a later file changes only Codex. Removing a rule from a spec removes it from Claude Code's `settings.json` on the next sync. Rules you wrote there by hand stay ([Claude settings](https://agnostic-ai.org/docs/targets/claude-reference/#claude-settings)). `agnostic-ai compare claude codex` reports each portable permission rule, including partial mappings, native overrides, output prerequisites, and that `permissions.default-mode` is written only by `sync --global`. It does not compare other settings fields. `sync --global` also reads settings specs from your home folder, for `model`, `effort`, tool-specific keys, `permissions.default-mode`, and, for Claude Code only, the `permissions` lists. See [default model and effort](https://agnostic-ai.org/docs/global-configuration/#global-default-model-and-effort). ## Effort by target `effort` reaches the tools below. A value the tool does not accept, or `effort` on any other tool, raises a sync note instead. `x-` wins, so `x-claude.effortLevel` overrides the portable value. `import` fills `effort` from these keys when the tool accepts the value and no other settings spec for that tool sets it; otherwise the value goes under `x-`. Kimi import does not read effort. | Target | Native key | Accepted values | |---|---|---| | Claude Code | `effortLevel` in `.claude/settings.json` | `low`, `medium`, `high`, `xhigh` | | Copilot | `effortLevel` in `.github/copilot/settings.json` | `low`, `medium`, `high`, `xhigh` | | Codex | `model_reasoning_effort` in `.codex/config.toml` | any string; `outputs.codex.config.model-reasoning-effort` and the captured overlay win | | CodeBuddy | `reasoningEffort` in `.codebuddy/settings.json` | `minimal`, `low`, `medium`, `high`, `xhigh`, `max` | | Factory | `reasoningEffort` in `.factory/settings.json` | `none`, `dynamic`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; each model accepts a subset | | Junie | `effort` in `.junie/config.json` | any string | | Kimi | `[thinking].effort` in the `config.toml` that `outputs.kimi.conf-file` names | any value | | Pi | `defaultThinkingLevel` in `.pi/settings.json` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` | ## Support by target | Target | `permissions` | `model` | |---|---|---| | Claude Code, Qoder, Kilo Code, OpenCode | yes | yes | | Factory | `Bash` rules only; `ask` goes to `commandDenylist` (which prompts), `deny` to `commandBlocklist` | yes | | Codex | `Bash` rules via `outputs.codex.exec-policies-from-permissions` ([translation](https://agnostic-ai.org/docs/targets/codex-reference/#translate-bash-permissions)); otherwise a note | yes | | Windsurf | yes | no | | Augment | `allow` and `deny` only, for whole `read`, `edit`, and `write` tools | no | | Cursor | `allow` and `deny` in `.cursor/cli.json`, for the Cursor CLI ([details](https://agnostic-ai.org/docs/targets/cursor/#permissions)) | no | | Copilot, Junie, Gemini | no; Copilot and Junie report the whole policy in a note | yes | Every other target takes neither. A field a tool cannot hold raises a sync note, and the other tools still get their files. A rule the vendor cannot scope, such as a path rule on Factory's shell lists, raises a note instead of widening. A shared Claude model name raises the same note as an [agent `model`](https://agnostic-ai.org/docs/spec-format/agents/#per-target-model-and-effort) on Codex, Gemini, OpenCode, Kilo Code, and Factory; Codex skips it when `outputs.codex.config.model` or the [captured overlay](https://agnostic-ai.org/docs/targets/codex-reference/#codex-config) sets the model. Review an imported `model` before you enable more tools, because model names differ between vendors. ## Target-specific keys Tool-specific keys go under `x-` and merge into that tool's settings file (`x-factory.sandbox` reaches `.factory/settings.json`). When sync also writes the same key: - Lists are joined, with the entries sync writes first. - Objects merge key by key, at every level. - A single value such as `model` is replaced. - A list and a string cannot merge. The `x-` value wins, with a sync note. - Maps of whole records (`x-qoder.mcpServers`, `x-augment.mcpServers`) merge by name. A server both sides name comes whole from the `x-` block. - Blocks with a fixed order (`x-qoder.hooks`, `x-augment.hooks`) keep their order, with your own events added at the end. Four keys take one type each: `x-augment.toolPermissions` a list, and `x-windsurf.permissions`, `x-kilo.permission`, and `x-opencode.permission` an object. Any other type is skipped with a sync note naming the key, and the rules from your `permissions` are still written. Exceptions: - Codex: `.codex/config.toml` comes from the captured overlay plus `outputs.codex.config`, so an `x-codex` block raises a sync note that names both. - Ellipsis reads no portable field, only `x-ellipsis`: its reviewer, pull request filters, posting rules, and budget go to `.ellipsis/code-review.yaml`, and the environment, its repository, and native options to `.ellipsis/environments/.yaml`. A later spec replaces each top-level block it sets ([details](https://agnostic-ai.org/docs/targets/ellipsis-reference/#how-specs-map-to-the-file)). - OpenCode V2 MCP imports use `x-opencode.mcp` to keep native connection details, disabled states, OAuth settings, and separate timeouts. Keep it in a settings spec, not in MCP specs. See [OpenCode](https://agnostic-ai.org/docs/targets/opencode/). ## Edit scalar settings in the browser Run `agnostic-ai serve --open` in a project, or `agnostic-ai serve --global --open` for user settings. The Settings view saves, updates, and clears a `model` or `effort` value for every tool, or for one tool under **What each tool gets**, which writes `model.` or `effort.`. Only the global view edits `session-retention-days`. The first save creates `settings/defaults.yaml` when no settings spec exists; with several, choose the file. `x-*` keys, nested values, and a `model` that names a model tier need the source file. A personal value for one tool writes only that tool's entry; the shared value still reaches the other tools (see [local overrides](https://agnostic-ai.org/docs/local-overrides/)). A browser save does not sync: run `agnostic-ai sync` or `agnostic-ai sync --global` afterward. # Reviews `reviews/` defines project checks for code-review bots, such as dependency boundaries, error handling, and migration risks. One review spec writes that guidance into each bot's own file: | Target | Reads | |--------|-------| | [Cursor](https://agnostic-ai.org/docs/targets/cursor/) (Bugbot) | `.cursor/BUGBOT.md`, and `/.cursor/BUGBOT.md` for scoped specs | | [Codex](https://agnostic-ai.org/docs/targets/codex/) (code review) | a `## Code Review Rules` section of the root or scoped `AGENTS.md` | | [Goose](https://agnostic-ai.org/docs/targets/goose/) (`goose review`) | `.agents/REVIEW.md`, and `/.agents/REVIEW.md` for scoped specs | | [Ellipsis](https://agnostic-ai.org/docs/targets/ellipsis/) (code review) | one reviewer's prompt in `.ellipsis/code-review.yaml`; scoped specs become sections that apply only to changes in their directory | Other tools report review specs as unsupported. Ellipsis takes the reviewer, its model, and any posting rules from `x-ellipsis` in a [settings spec](https://agnostic-ai.org/docs/spec-format/settings/). Without a reviewer, sync writes no file and says what is missing. ## Write one ```bash agnostic-ai new review code-quality ``` It creates `.agnostic-ai/reviews/code-quality.md`. Write the checks the bot should apply, then sync and start a review. The spec supplies guidance only; it does not install or start a bot. ```markdown --- scope: backend --- Flag any handler that talks to the database directly instead of going through a repository. ``` Reviews follow `scope` and the folder layout like [rules](https://agnostic-ai.org/docs/spec-format/rules/). Specs with the same scope are joined into one file, without frontmatter. The Codex section sits in `AGENTS.md`, so every tool that reads `AGENTS.md` loads it, unless a `targets:` filter leaves out `codex`. ## Include a file A line holding only `@path` includes that file, read from the project root, so a folder's `README.md` can be its review: ```markdown --- scope: services/billing target: cursor --- @services/billing/README.md ``` - Sync writes the file's text in place of the line, so a README change shows up in `sync --check`. - A missing file, an absolute path, or a path outside the project fails the load ([AAI-001](https://agnostic-ai.org/docs/errors/#aai-001-spec-parse-failed)). - A line inside a fenced code block stays as written. - An included file is not searched for more `@path` lines. - Only reviews take `@path` lines. On `AGNOSTIC_AI.md` the [`resolve-imports`](https://agnostic-ai.org/docs/configuration/#syncresolve-imports) setting controls them. # Environments `environments/` defines project setup for a new Git worktree (a separate checkout of the same repository), cloud sandbox, or preview. It covers installing dependencies, copying local config, starting the dev server, and stopping it afterwards. Sync writes each tool's native file, such as `.cursor/worktrees.json`, `.codex/environments/environment.toml`, or `.claude/launch.json`. ## Write one ```bash agnostic-ai new environment development ``` It creates `.agnostic-ai/environments/development.yaml` with no commands. Add the commands you have reviewed, and create any script they call before syncing: ```yaml name: dev setup: bash scripts/setup-worktree.bash cleanup: bash scripts/stop-preview.bash dev-commands: - name: Docs command: [pnpm, dev:mintlify] cwd: apps/docs port: 3000 auto-port: true ``` A sandbox that installs once and keeps a terminal open (Cursor, OpenHands, Amp): ```yaml install: go mod download terminals: - name: dev command: go run ./cmd/agnostic-ai ``` ## Fields - `setup`: the commands a tool runs in a new worktree (one command or a list). `setup-windows` replaces it on Windows. - `cleanup`: the commands a tool runs when it removes the worktree. - `install` and `terminals`: a sandbox's install step and its long-running processes. - `dev-commands`: dev servers a tool can start and preview. Each entry needs a unique `name` and a `command` (a string or a list of words). `cwd` (relative to the project root), `port`, `auto-port`, `env`, `url`, and `icon` (the Codex button icon, `run` by default) are optional. An entry with a `url` and no `command` tells Claude Code to open a server you already run instead of starting one; Codex has no such action, so sync skips the entry there with a note. - A string command with shell syntax (pipe, several lines, `VAR=value`, a builtin like `cd`) runs through `sh -c`, which on Windows needs a POSIX shell such as Git Bash on the `PATH`. A list runs with no shell. `env` values are written as text. - Specs merge by top-level key, and the last value wins. A field that no enabled tool uses gets a no-effect note. - `lint` reports a dev command with no `name`, with neither `command` nor `url`, a repeated name, an unknown key, or a value of the wrong type (LINT016). It also reports a `url` entry with no `command` that reaches Codex, and names the target. Give Codex its own list without that entry under `x-codex.dev-commands`. ## Support by target Fields a tool does not use get a no-effect note. Other tools report the spec as unsupported. Each tool's page has the key mapping. | Target | Uses | Writes | |---|---|---| | [Cursor](https://agnostic-ai.org/docs/targets/cursor/) | `setup`, `setup-windows`; every other key except `name`, `scope`, `description`, the target fields, `dev-commands`, and `cleanup` | `.cursor/worktrees.json`, `.cursor/environment.json` | | [Codex](https://agnostic-ai.org/docs/targets/codex/) | `setup`, `setup-windows`, `cleanup`, `dev-commands` (as scripts and action buttons) | `.codex/environments/environment.toml` | | [Claude Code](https://agnostic-ai.org/docs/targets/claude/) | `setup` ([worktree setup](#claude-code-worktree-setup)), `dev-commands` (a relative `cwd` reads from the project root, so no `${workspaceFolder}`) | `.claude/hooks/agnostic-ai-worktree-setup.sh`, `.claude/launch.json` | | [OpenHands](https://agnostic-ai.org/docs/targets/openhands/) | `install` (a setup script) | `.openhands/setup.sh` | | [Amp](https://agnostic-ai.org/docs/targets/amp/) | `install` (a setup script), `terminals` (services) | `.agents/setup`, `.amp/services.yaml` | | [Ellipsis](https://agnostic-ai.org/docs/targets/ellipsis/) | `install` as `hooks.build_base`, `setup` as `hooks.after_checkout`, each after `cd /sandbox/`, only for the environment `x-ellipsis.environment` selects | `.ellipsis/environments/.yaml` | `import cursor`, `import codex`, and `import ellipsis` read their files back. A successful install does not check the generated files: run the [project readiness checks](https://agnostic-ai.org/docs/cli-reference/project/#project-readiness) after installation and sync, before starting a cached environment. ## Claude Code worktree setup {#claude-code-worktree-setup} Claude Code has no setup step for a worktree it creates ([worktrees](https://code.claude.com/docs/en/worktrees#set-up-the-worktree-environment)), so sync writes `setup` into `.claude/hooks/agnostic-ai-worktree-setup.sh` and runs it from three hooks in `.claude/settings.json`: - `SessionStart` with matcher `startup`, for `claude --worktree` and Desktop worktree sessions. - `SubagentStart`, for a subagent with `isolation: worktree`. `SessionStart` does not fire for a subagent. - `PostToolUse` with matcher `EnterWorktree`, for a worktree Claude enters during a session. Each hook sets `shell: bash`, so on Windows the script needs Git Bash, or setup does not run. A checkout without the script, such as a worktree created before the first sync, skips the hook. Claude Code runs matching hooks in parallel, so delete a hand-written `SessionStart` hook that installs the same dependencies. How the script runs: - It takes the worktree from the `cwd` in the hook's event data, because `$CLAUDE_PROJECT_DIR` stays at the directory the session started in. - It runs `setup` from the worktree root through `sh`, only in a linked Git worktree, never in the main checkout. - A successful run leaves a marker in the worktree's own Git directory, so later sessions there skip setup. A failed run, or a worktree made before the first sync with `setup`, has no marker, so the next session, subagent, or worktree entry runs it. - Claude Code stops a command hook after 600 seconds by default, so a longer setup fails and runs again next time. - Setup output goes to stderr, because Claude Code adds a `SessionStart` hook's stdout to the session context. - The script exits when `AGNOSTIC_AI_TARGET` is not `claude`. A tool that also reads the hooks in `.claude/settings.json`, such as Cursor, then does not run setup a second time. ### Refresh an existing worktree After changing setup or dependencies, sync one worktree and rerun its Claude setup: ```bash agnostic-ai hook worktree-refresh --repo /path/to/main --worktree "/path/to/linked worktree" ``` It resets only that linked worktree's marker and runs setup again; a failure leaves no marker. See [worktree refresh](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-worktree-refresh) for what it checks and refuses. ### Include setup in new worktrees A new worktree needs `.claude/settings.json` and the script. - With `gitignore.enabled`, both are ignored. `.worktreeinclude`, which sync keeps when `claude` is a target, copies them into each worktree Claude Code creates (see [gitignore](https://agnostic-ai.org/docs/configuration/#gitignore)). - Without it, both files are committed, so the checkout has them. To keep `setup` for the other tools and set up Claude Code another way, turn the hooks off: ```yaml setup: composer install x-claude: setup: false ``` ### Claude cleanup limits `cleanup` has no Claude Code equivalent. Claude Code documents `WorktreeRemove` as the removal step for a worktree a `WorktreeCreate` hook made. `SessionEnd` runs at the end of every session with a 1.5-second budget. ## Import When several tools keep their own environment file, `import` limits each spec to its tool with `targets: []`, so the next sync writes every file back as it was. Remove the line to share a spec. A project with one tool's environment file gets one shared spec. # Ignore `ignore/` lists paths supported tools leave out of context or indexing, such as generated code or large fixtures. An ignore spec writes the same patterns into each tool's own ignore file, such as `.cursorignore`, `.geminiignore`, `.aiignore`, or `.aiderignore`. Claude Code and Codex get none; the [target support table](https://agnostic-ai.org/docs/targets/#capability-matrix) lists who does. Depending on the tool, an ignore file covers reads, indexing, or shell commands. It does not block access on its own; use [permissions](https://agnostic-ai.org/docs/spec-format/settings/#support-by-target) for that. ## Write one ```bash agnostic-ai new ignore generated-files ``` The template excludes nothing. Put gitignore-syntax patterns in the body, one file per group, using exact paths or narrow patterns. ````markdown Paths to leave out of context or indexing. ```gitignore *.env secrets/ dist/ ``` ```` With fenced code blocks, only the lines inside them are patterns, and the text around them is prose. A body without a fence is read whole. Prettier strips trailing spaces inside a block, so write a name ending in a space as `name[ ]`. Comment and blank lines exclude nothing; `#` starts a comment only at the start of a line. ## Output Specs are joined into each tool's ignore file under a `#` header naming the source, with a blank line between them. Order and whitespace are kept, and CRLF becomes LF. Override the path with `outputs..ignore-file`. Tools without an ignore file report the spec as unsupported. ## Overwrite behaviour Sync replaces an ignore file that has no agnostic-ai generated-file header only when every existing pattern stays, unchanged and in order. Extra patterns are fine. Missing or reordered patterns, added negations (`!pattern`), and changed whitespace fail with [`AAI-103`](https://agnostic-ai.org/docs/errors/#aai-103-hand-authored-ignore-file-would-lose-patterns) and leave the file untouched, even when the rewrite means the same thing. Run `agnostic-ai import ` to copy the patterns into `ignore/.md` as a fenced block, with comments, order, and whitespace kept and a leading UTF-8 byte-order mark dropped. The spec sets `target: `; remove it to share the patterns with every tool, and check the combined order if other specs add negations or reorder patterns. `outputs..provenance-header: false` removes the header and turns this check off. A dry run skips the check; `sync --check` still reports unsafe overwrites. # Overlays `overlays/` keeps the parts of a Claude Code or Codex configuration that no shared spec covers yet, such as a Claude Code `statusLine`, Codex `[tui]` settings, or a status-line script. `agnostic-ai import` saves them here, and `sync` writes them back into the tool's files, restoring a deleted `.claude/` or `.codex/` directory with those keys. Import leaves out of `claude.settings.json` each `env` value, helper command such as `apiKeyHelper`, and `statusLine` command that holds a credential, and any literal value of a header variable such as `OTEL_EXPORTER_OTLP_HEADERS`, and prints a warning naming the key. The [setting value rule](https://agnostic-ai.org/docs/spec-format/mcp-import/#setting-values) decides which `env` values hold a credential. The next sync then removes it from `.claude/settings.json`. To keep it, set it under `x-claude` in a settings spec in [`local/`](https://agnostic-ai.org/docs/local-overrides/), which Git ignores. To change an imported setting, edit its overlay, check `agnostic-ai sync --dry-run`, then sync. An overlay reaches one tool only; move a setting into a portable spec when other tools should get it. ## Files | Overlay | Written into | Holds | |---------|--------------|-------| | `claude.settings.json` | `.claude/settings.json` | Every key import did not move into a spec, such as `statusLine` | | `claude.settings.hook-events.json` | `.claude/settings.json` | The keys that sat next to `hooks`, and the order of its events, so the `hooks` block keeps both | | `codex.config.toml` | `.codex/config.toml` | The keys import did not move into a spec, such as `sandbox`, `[history]`, and `[tui]`. Keys Codex ignores in a project config stay here but are not written ([list](https://agnostic-ai.org/docs/targets/codex-reference/#keys-codex-ignores-in-a-project-config)) | | `codex.exec-policies.yaml` | `.codex/rules/default.rules` | Every `prefix_rule(...)` from the imported file | | `codex.exec-policies-header.txt` | `.codex/rules/default.rules` | The comment block above the first rule | | `claude/`, `codex/` | `.claude/`, `.codex/` | Helper files: Claude `CLAUDE.md`, `README.md`, and `statusline.sh`; Codex `README.md`. File modes are kept, so a script stays executable | Sync reads only these files. Any other file here, such as `gemini.settings.json` or a misspelled `claude.setings.json`, gets a sync note and a `lint` warning (LINT051). Dot files, dot folders, and README files at the top are skipped. For Gemini CLI and the other tools that merge `x-` from a [settings spec](https://agnostic-ai.org/docs/spec-format/settings/), put the keys there; Cursor's settings spec drops that block. `sync --watch` re-runs when an overlay changes. The target pages say which source wins: [Claude settings](https://agnostic-ai.org/docs/targets/claude-reference/#claude-settings), [Codex config](https://agnostic-ai.org/docs/targets/codex-reference/#codex-config), and [Codex exec policies](https://agnostic-ai.org/docs/targets/codex-reference/#codex-exec-policies). --- # Targets A target writes configuration for one coding tool. Filter the table, then open a tool for its paths, import coverage, and native limits. New here? [Create one rule for Claude Code and Codex](https://agnostic-ai.org/docs/getting-started/#first-rule). ## Tool support {#capability-matrix} Some tools are left out of the default target list and must be named in `targets`; see [select project targets](https://agnostic-ai.org/docs/configuration/#targets). ## Tools without a target Many tools read the root `AGENTS.md` and `.agents/skills/`. Enable a target that writes both, such as [codex](https://agnostic-ai.org/docs/targets/codex/) with its default output paths. Those tools then get your project-wide rules and skills. Codex includes project-wide rules in `AGENTS.md`; see [cross-target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). Check the tool's own discovery rules before relying on those shared paths. MCP servers, commands, and permissions need its native configuration; enabling a compatible target does not translate them for an unknown tool. To ask for full support, [open an issue](https://github.com/Chemaclass/agnostic-ai/issues). ## Related reference - [Compare two or three targets](https://agnostic-ai.org/docs/compare/) - [Release updates](https://agnostic-ai.org/updates/): dated tool news. A feature a tool announces is supported only once its target page says so. - [Select project targets](https://agnostic-ai.org/docs/configuration/#targets) - [Understand cross-target behavior](https://agnostic-ai.org/docs/target-behavior/) - [Use directory-specific instructions](https://agnostic-ai.org/docs/scoped-context/) - [Define specs shared across tools](https://agnostic-ai.org/docs/spec-format/) - [Add a new adapter](https://github.com/Chemaclass/agnostic-ai/blob/main/docs/internal/adding-adapters.md) # Claude Code (`claude`) Write a project rule once under `.agnostic-ai/`. Sync gives Claude Code its native `.claude/rules/` file and gives other selected tools their own format. Skills, agents, hooks, MCP servers, and settings also have native outputs. Using this provider through Composer? Follow the [Composer setup guide](https://agnostic-ai.org/docs/composer/). ## First rule Install the client using the [official setup guide](https://code.claude.com/docs/en/quickstart), then open your project in it. Select `claude` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule). The shared commit-prefix rule becomes `.claude/rules/conventional-commits.md`. If you also select Codex, the same source becomes a section in `AGENTS.md`. Write shared project instructions in `.agnostic-ai/AGNOSTIC_AI.md`. Sync writes Claude's `CLAUDE.md` from that source. Edit the source file for later changes. Already have Claude files? Check [import coverage](https://agnostic-ai.org/docs/targets/claude-reference/#import) before syncing. Import reads native rules, agents, skill folders, commands, hooks, permissions, MCP servers, and a settings overlay. It reports files it cannot import. ## Output | Source | Default output | |---|---| | Shared instructions | `CLAUDE.md` | | Rules | `.claude/rules/.md` | | Skills and bundled files | `.claude/skills//SKILL.md` | | Agents | `.claude/agents/.md` | | Commands | `.claude/commands/.md` | | Hooks and settings | `.claude/settings.json`, when configured | | MCP servers | `.mcp.json`, when configured | | Local preview commands | `.claude/launch.json`, when configured | An empty kind creates no corresponding files. For output overrides and field mappings, use the [native reference](https://agnostic-ai.org/docs/targets/claude-reference/). ## Loading and limits - Claude Code [discovers `.claude/rules/` recursively at launch](https://code.claude.com/docs/en/memory). Sync translates file patterns into native `paths` conditions. Start a new session after changing the setup, and check the rule is not excluded by native settings. - When another selected tool writes identical shared instructions to `AGENTS.md`, `CLAUDE.md` can import it with `@AGENTS.md`. Starting in a subfolder can require import approval; non-interactive `claude -p` skips that outside import. See [instruction loading](https://agnostic-ai.org/docs/targets/claude-reference/#rules-and-instruction-loading). - Claude Code lists every agent description at startup and warns when the total passes 15,000 tokens, but still loads every agent. `lint` estimates that total; see [context budgets](https://agnostic-ai.org/docs/cli-reference/lint-codes/#context-budgets). - Explore and Plan start without your `CLAUDE.md` files. A rule they must follow, such as "ignore the vendor/ directory", has to be restated in the prompt Claude gives them when it delegates. A `CLAUDE.md` in a subdirectory still loads when they read files there. - An [agent-only MCP server](https://agnostic-ai.org/docs/spec-format/mcps/#agent-only) is written into the agent file, not `.mcp.json`. Claude Code loads it only after you trust the project folder in an interactive session. A parent folder's trust does not count, and neither does the trust a `claude -p` or SDK session gets. Until then the agent runs without the server ([Claude Code docs](https://code.claude.com/docs/en/sub-agents#inline-server-trust)). - A read-only agent blocks Claude's edit tools but still allows Bash. Hooks and permissions also differ across tools; check the native mapping before treating shared text as an enforced restriction. With `builtins: [memory]`, shared memory adds to [Claude Code's own memory](https://agnostic-ai.org/docs/memory/#claude-code-s-own-memory). For home instructions and user settings, see [global configuration](https://agnostic-ai.org/docs/global-configuration/). A home `voice` setting writes `voice.enabled` and `voice.mode` to `~/.claude/settings.json`; `voice.language` is not written, because Claude takes its dictation language from its response language. See [voice dictation](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation). ## Protected paths A settings spec's `protected` block becomes `Edit(/)` rules in `.claude/settings.json`, under `ask` or `deny`. The leading `/` anchors them at the project root. Removing a path removes its generated rule on the next sync. A deny also covers file commands Claude recognizes in Bash, such as `sed` and redirections, but cannot block an arbitrary script that writes files. An ask guards edit tools only. See [protected paths](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). ## Verify Run `agnostic-ai sync --check` and inspect `.claude/rules/conventional-commits.md`. Start `claude` from the project root. Ask for a commit subject for a small fix; it should use `fix:`. To check what Claude Code loaded from `.claude/agents/`: - `claude plugin validate .claude/agents` lists agent files whose frontmatter does not parse. It needs Claude Code 2.1.233 or later, checks only the directory you name, and does not flag a file that parses but has no `name`. - `claude --debug` writes a log to `~/.claude/debug/.txt`. Search it for `does not recognize or apply` to find misspelled agent fields. It needs Claude Code 2.1.296 or later. For a file-specific rule, run `agnostic-ai explain --file src/main.go --target claude` to inspect the source and planned output. Confirm native loading with both a matching and an unrelated file in a new session. ## Run Explore on a cheaper model A project agent named `Explore` replaces Claude Code's built-in Explore and keeps its own `model`. Write one that only Claude Code gets: ```markdown --- name: Explore target: claude description: Finds files and code for broad lookups. Use proactively before reading many files. model: haiku can: [read, Grep, Glob] --- Search only. Return file paths with one line each. ``` Save it as `.agnostic-ai/agents/explore.md`. `target: claude` keeps the agent away from Junie, which rejects a name that starts with a capital letter. The `description` is required: Claude Code skips an agent file that has none. Next, [add a reusable skill](https://agnostic-ai.org/docs/spec-format/skills/) or compare the same setup in [Codex](https://agnostic-ai.org/docs/targets/codex/). # Claude Code native reference Start with the [Claude Code setup](https://agnostic-ai.org/docs/targets/claude/). This page covers native mappings and overrides. For shared home configuration and ownership, see [global configuration](https://agnostic-ai.org/docs/global-configuration/). ## Rules and instruction loading Claude Code reads `.claude/rules/**/*.md` natively. Sync translates `globs` or `paths` into `paths:` frontmatter and removes portable activation keys. A comma-separated string produces one pattern per entry. A directory scope and file patterns add together: `scope: src/a` with `globs: tests/a/**` covers both directories. Omit `scope` for a file-only filter, and keep the source outside a folder that implies scope. `CLAUDE.md` carries the shared instructions. When another target writes an identical root `AGENTS.md`, Claude's file imports it with `@AGENTS.md`, plus Claude-only text. Sync keeps a full copy if a rules appendix or target-specific text would make the files differ. If the existing Claude file already imports `AGENTS.md`, sync can keep that layout without another target. A session started in a subfolder treats the root `@AGENTS.md` as an outside import. Interactive sessions ask once; non-interactive `claude -p` skips it. Use [explain](https://agnostic-ai.org/docs/cli-reference/inspect/#explain-a-source-file) to inspect planned files, then confirm native loading in that working directory. Legacy `rules-mode: import` is for versions without native rules loading. `rules-file: CLAUDE.md` joins rule bodies there; another `rules-file` path gets an import from `CLAUDE.md`. ## Agents, skills, and commands | Source field | Claude Code output | |---|---| | Skill `x-claude` | Native skill frontmatter; overrides portable fields | | Skill `model`, `effort` | Single value or per-target map | | Command body | `.claude/commands/.md`, invoked as `/` | | Agent frontmatter | Passed through, including resolved `model` and `effort` | | Agent `readonly: true` | `disallowedTools: Write, Edit, NotebookEdit`; Bash stays allowed | A declared `disallowedTools` or `x-claude.disallowedTools` wins over `readonly`. A null override omits it. Claude Code skips names starting with `-` or containing `:`; sync rejects them before writing, including names from `x-claude`. For global agents and user settings, see [global configuration](https://agnostic-ai.org/docs/global-configuration/). ## Hook mappings Hooks merge into `.claude/settings.json`. See the [native hook reference](https://code.claude.com/docs/en/hooks) for event names and payloads, and [portable hooks](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) for shared events. | Handler | Fields | |---|---| | Command | `command`; `args` selects exec form; command lists become separate handlers | | HTTP | `url`, optional `headers`, `allowedEnvVars` | | MCP tool | `server`, `tool`, optional `input` | | Prompt | `prompt`, optional `model`, `continueOnBlock` | Handlers keep `timeout`, `statusMessage`, `if`, and `once`. `args`, `async`, `asyncRewake`, and `shell` are command-only. `once` has no effect in settings hooks; Claude Code honors it only in skill frontmatter. Experimental agent handlers are not emitted. Claude Code runs some handlers only on some events. Prompt handlers run on tool, permission, `UserPromptSubmit`, `UserPromptExpansion`, `Stop`, `SubagentStop`, `TaskCreated`, `TaskCompleted`, and `TeammateIdle` events. HTTP handlers run everywhere except `SessionStart` and `Setup`. MCP tool handlers never run on `Setup`. `if` works only on `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, and `PermissionDenied`; on another event the hook never runs. Sync still writes such a hook and prints a note. See the [hook types per event](https://code.claude.com/docs/en/hooks). `failClosed: true` becomes `onFailure: "block"` for command and HTTP handlers. Use synchronous `PreToolUse` or `UserPromptSubmit` when a failed check must stop an action. Claude ignores it for `Stop`, `SubagentStop`, `TaskCompleted`, `TeammateIdle`, or async command handlers. Import retains other native `onFailure` values under `x-claude`; portable `failClosed: true` wins. With command hooks, sync sets `AGNOSTIC_AI_TARGET: claude` in settings `env`; an explicit settings value wins. When Cursor is also selected, a portable hook's Claude copy exits under Cursor to avoid running twice. Other compatibility reads need [cross-target checks](https://agnostic-ai.org/docs/target-behavior/#spec-kind-behavior). ## MCP mappings `.mcp.json` is rebuilt from MCP specs on every sync. Import hand-written servers before syncing. | Transport | Native fields | |---|---| | Stdio | `command`, `args`, `env`; no `type` | | Remote | `type`, `url`, `headers`; also `headersHelper` and `oauth` for HTTP, SSE, and WS | | Either | `timeout` (milliseconds per tool call; values below 1000 ignored), `roots` | `alwaysLoad` and `bareElicitationCapability` preserve explicit true and false; `x-claude` overrides them. They are Claude-only fields. See the [MCP reference](https://code.claude.com/docs/en/mcp) for their native behavior. Remote OAuth keeps `clientId`, `callbackPort`, `authServerMetadataUrl`, and space-separated `scopes`. Sync never writes `oauth.clientSecret`; Claude Code keeps it in the system keychain. `disabled: true` writes `disabledMcpjsonServers` in project settings. It requires the project `.mcp.json` path; a custom path fails. Sync records only its generated rejections in `.claude/.agnostic-ai-mcp-disabled.json` and removes the entry when re-enabled. ## Environment mappings `dev-commands` become `.claude/launch.json` preview servers: command splits into `runtimeExecutable` and `runtimeArgs`, `auto-port` becomes `autoPort`, and `cwd` stays project-relative. An entry with a `url` and no `command` becomes `{name, url}`, which attaches the preview to a server you already run. `x-claude.autoVerify` sets `autoVerify`. Import normalizes `${workspaceFolder}` paths and reads a URL-only configuration back as a dev command without `command`. `setup` becomes `.claude/hooks/agnostic-ai-worktree-setup.sh`. Session, subagent, and EnterWorktree hooks run it once per new linked worktree, using Bash. Windows needs Git Bash. `x-claude.setup: false` omits them. Other environment fields have no Claude output and get a note. See [worktree setup](https://agnostic-ai.org/docs/spec-format/environments/#claude-code-worktree-setup). ## Config keys | Key | Default | Notes | |---|---|---| | `outputs.claude.dir` | `.claude` | | | `outputs.claude.rules-dir` | `.claude/rules` | auto-loaded by Claude Code | | `outputs.claude.rules-mode` | unset | set to `import` to wire `.claude/rules/*.md` into `CLAUDE.md` via `@`-imports; only for Claude Code versions without native rules loading | | `outputs.claude.rules-file` | unset | legacy single-file layout, typically `CLAUDE.md`; any other path keeps `CLAUDE.md` and wires the file in with an `@`-import | | `outputs.claude.commands-dir` | `.claude/commands` | | | `outputs.claude.agents-dir` | `.claude/agents` | | | `outputs.claude.skills-dir` | `.claude/skills` | | | `outputs.claude.mcp-file` | `.mcp.json` | | | `outputs.claude.settings` | | settings block for `.claude/settings.json` | `import claude` and `agnostic-ai doctor` read the resolved paths, so a moved directory still imports and syncs back unchanged. `dir` moves the whole tool directory: with `dir: vendor/.claude`, rules land in `vendor/.claude/rules/`, commands in `vendor/.claude/commands/`, and {{rules_dir}} and other path variables resolve there. Per-kind keys override their own path. The next full sync removes old files as orphans. Claude Code auto-loads only a project-root `.claude/rules/`, so a moved rules directory needs `rules-mode: import`. With `gitignore.enabled`, the managed `.gitignore` block also lists `/.claude/agent-memory-local/`, `/.claude/settings.local.json`, `/.claude/worktrees/`, and `/.claude/scheduled_tasks.lock`, following `outputs.claude.dir`. It omits `.claude/agent-memory/` (`memory: project`), which is shareable. Ignoring does not untrack: run `git rm -r --cached .claude/agent-memory-local` if that store is committed. The block also goes into `.worktreeinclude`, so new worktrees start with the ignored outputs ([gitignore](https://agnostic-ai.org/docs/configuration/#gitignore)). `gitignore.ignore-worktree-include: true` ignores the managed `.worktreeinclude` too; `gitignore.worktree-include: false` leaves it to you. ## Agent memory A top-level `memory` key gives a subagent a directory that persists across sessions. Claude Code acts on it; support in other tools differs. See [agent memory fields](https://agnostic-ai.org/docs/spec-format/agents/). ```yaml --- name: code-reviewer description: Reviews diffs for bugs and style. memory: project --- ``` | Scope | Directory | Git | |---|---|---| | `user` | `~/.claude/agent-memory//` | outside the repository | | `project` | `.claude/agent-memory//` | shareable; commit it if the team wants it shared | | `local` | `.claude/agent-memory-local//` | do not commit | Claude Code creates the directory on first use. Subagent memory is part of auto memory: with `autoMemoryEnabled` off or `CLAUDE_CODE_DISABLE_AUTO_MEMORY` set, the `memory` field has no effect. With auto memory on, Claude Code turns on Read, Write, and Edit for that agent so it can manage its memory files. Session auto memory (`~/.claude/projects//memory/`) is separate. Sync leaves it alone unless the [`memory` built-in](https://agnostic-ai.org/docs/memory/#claude-code-s-own-memory) points it at the shared personal store. That needs auto memory on: with `autoMemoryEnabled` off or `CLAUDE_CODE_DISABLE_AUTO_MEMORY` set, Claude saves nothing there. See [shared memory](https://agnostic-ai.org/docs/memory/). ## Claude settings `sync --global --only claude` maps a home settings spec's `permissions.default-mode` to `permissions.defaultMode` in `~/.claude/settings.json`. It merges the home's `allow`, `ask`, and `deny` rules into the lists there. `import --global claude` copies user-level rules into the home. See [global settings and rule ownership](https://agnostic-ai.org/docs/configuration/#global-default-model-and-effort). `outputs.claude.settings` declares `.claude/settings.json` keys directly. Layers, lowest first (a later layer wins): 1. Captured overlay (from `import claude`). 2. Agnostic `settings` specs (`.agnostic-ai/settings/`), the cross-tool source for `permissions`, `model`, and `effort`. `effort` lands as `effortLevel` when it is `low`, `medium`, `high`, or `xhigh`. 3. This `outputs.claude.settings` block. 4. The `hooks` block built from your hook specs. 5. An `x-claude` block on a settings spec. Unset keys fall through to the layer below. `x-claude` merges: `x-claude.permissions.deny` adds to the translated deny list, and only single values such as `model` are replaced. Without a captured overlay, sync merges permission lists into the on-disk `settings.json` and records the rules it added in `.claude/.agnostic-ai-permissions.json`. The next sync removes those first, so a rule dropped from a spec, or moved from `allow` to `deny`, leaves the file. Hand-written and overlay rules stay. The first sync without a record keeps every rule and records the spec ones. ```yaml outputs: claude: settings: attribution: commit: "" pr: "" ``` | Field | Type | Notes | |-------|------|-------| | `model` | string | Default model preference for new sessions. | | `outputStyle` | string | One of the Claude Code output styles. | | `apiKeyHelper` | string | Path to a script that prints an API key on stdout. | | `cleanupPeriodDays` | integer | Days of conversation history to retain. | | `bashOutputMaxChars` | integer | Inline command output limit before it spills to a file, up to 128000. Needs Claude Code v2.1.261 or later. | | `taskOutputMaxChars` | integer | Retired, not written. Sync removes a copy an earlier sync wrote and prints a note; delete it from `agnostic-ai.yaml`. A copy in `.agnostic-ai/overlays/claude.settings.json` is yours: sync keeps it and names it in a note. | | `attribution` | object | `commit` and `pr` set the attribution text for commits and pull requests; an empty string disables it. `sessionUrl` controls whether the session URL is included. | | `includeCoAuthoredBy` | boolean | Deprecated. Use `attribution`; when both are present, `attribution` wins. | | `enabledPlugins` | map of string to boolean | `plugin-id@marketplace-id` keys mapped to `true` to enable them. | | `env` | map of strings | Environment variables exported into Claude sessions. | | `statusLine` | object | `type`, `command`, and optional `padding`, `refreshInterval` (seconds, minimum 1), and `hideVimModeIndicator`. | | `permissions` | object | `allow`, `deny`, `ask` lists of tool-pattern strings. | Other settings survive import and sync through the overlay `agnostic-ai import claude` captures (`.agnostic-ai/overlays/claude.settings.json`). For a single value in both, this block wins. `permissions` `allow`/`deny`/`ask` lists combine across the overlay, `settings` specs, and this block. Claude Code's `/skills` menu saves per-skill toggles as `skillOverrides` in `.claude/settings.local.json`. `agnostic-ai doctor` reports synced skills that this file, the shared `.claude/settings.json`, or your user `~/.claude/settings.json` turns off, naming each file. `doctor --fix` offers to re-enable them in `.claude/settings.local.json` only. See [Claude skill overrides](https://agnostic-ai.org/docs/cli-reference/check/#claude-skill-overrides). `agnostic-ai explain ` shows the entries that name one skill, file by file; see [explain](https://agnostic-ai.org/docs/cli-reference/inspect/#claude-skill-overrides). To set visibility for the whole team, put `skillOverrides` in `.agnostic-ai/overlays/claude.settings.json`; sync writes it to `.claude/settings.json`. Claude Code ignores the following keys or values in a project `.claude/settings.json`. Sync still writes them from `x-claude` and prints a note. `sync --global` also writes them, and notes the managed-only keys, which Claude Code ignores in `~/.claude/settings.json` too. Where a key shows a value, only that value is ignored. Scopes come from the [settings reference](https://code.claude.com/docs/en/settings-reference). | Where Claude Code reads it | Keys | |---|---| | User settings (a home settings spec and `sync --global`) or managed settings | `appendPlugins`, `askUserQuestionTimeout`, `autoMode`, `axScreenReader`, `bashEditDiffEnabled: true`, `desktopSessionCleanupPeriodDays`, `dialogExpiry`, `feedbackDrafts`, `footerLinksRegexes`, `forceLoginGatewayUrl`, `modelPicker`, `pluginConfigs`, `prependPlugins`, `processWrapper`, `sandbox.allowAppleEvents`, `sandbox.allowNestedUserNamespaces: true`, `sandbox.credentials.allowPlaintextInject`, `sandbox.credentials.awsPairs`, `sandbox.credentials.sigv4`, `sandbox.filesystem.disabled`, `sandbox.network.strictAllowlist`, `sandbox.network.tlsTerminate`, `sandbox.ripgrep`, `skipAutoPermissionPrompt`, `skipDangerousModePermissionPrompt: true`, `spellcheck`, `sshConfigs`, `sshHostCommands`, `syncClaudeAiPlugins: false`, `syncClaudeAiSkills: false`, `useAutoModeDuringPlan: false`, `vimInsertModeRemaps` | | User settings only (a home settings spec and `sync --global`) | `worktree.location` | | Managed settings only | `allowAllClaudeAiMcps`, `allowClaudeInChromeWithManagedMcp`, `allowedChannelPlugins`, `allowedProviders`, `allowManagedHooksOnly`, `allowManagedMcpServersOnly`, `allowManagedPermissionRulesOnly`, `availableModelsMatch`, `blockedMarketplaces`, `browserExternalPageTools`, `channelsEnabled`, `claudeMd`, `deniedModels`, `disableBrowserExternalNavigation`, `disableCommandPluginSources`, `disableDesktopLocalSessions`, `disableMobileSimulatorTools`, `disableSideloadFlags`, `forceRemoteSettingsRefresh`, `gatewayInternalNetworks`, `managedMcpServers`, `managedSourcesBehavior`, `modelPricing`, `parentSettingsBehavior`, `pluginSuggestionMarketplaces`, `pluginTrustMessage`, `policyHelper`, `requiredMaximumVersion`, `requiredMinimumVersion`, `sandbox.bwrapPath`, `sandbox.filesystem.allowManagedReadPathsOnly`, `sandbox.network.allowManagedDomainsOnly`, `sandbox.socatPath`, `sshHostAllowlist`, `strictKnownMarketplaces`, `strictPluginOnlyCustomization`, `wslInheritsWindowsSettings` | `autoContinueAtUsageLimit` has a restrictive fallback: if user settings, `--settings`, and managed settings leave it unset, either value in project or local settings turns automatic continuation off. To enable it, use one of those trusted scopes ([setting reference](https://code.claude.com/docs/en/settings-reference#autocontinueatusagelimit)). A project or local `bashEditDiffEnabled: true` cannot enable Bash change recording. A `false` there can turn it off unless a higher-precedence file sets `true` ([setting reference](https://code.claude.com/docs/en/settings-reference#basheditdiffenabled)). ## Import `agnostic-ai import claude` reads the Claude Code files: | Source | Becomes | |--------|---------| | `.claude/rules/**/*.md` (preferred) | `//.md` (unchanged copy) | | `CLAUDE.md` rules block written by `sync` | `/.md` per rule (only when `.claude/rules/` is absent) | | `CLAUDE.md` (any form) | `.agnostic-ai/AGNOSTIC_AI.md` (unchanged copy) | | `AGENTS.md` or `.claude/AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md` (only when no `CLAUDE.md` exists) | | `CLAUDE.md` that imports `@AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md`: the `AGENTS.md` text, then the rest of `CLAUDE.md` in a `::target claude` fence (no rules) | | `/CLAUDE.md` (nested, hand-written) | one rule with the whole file and `scope: `, named after the scope: `api.md` for `services/api/`, or `services-api.md` when another scope also ends in `api` (only when `.claude/rules/` is absent) | | `/CLAUDE.md` that imports `@AGENTS.md` | the same rule, with the `AGENTS.md` text, then the rest of `CLAUDE.md` in a `::target claude` fence. When `codex` imports in the same run, only the fenced text lands, as `-claude.md` | | `.claude/agents/*.md` | `/.md` (unchanged copy, except a Claude model name becomes `model: {claude: }`) | | `.claude/skills//SKILL.md` | `//SKILL.md` (`allowed-tools` moves under `x-claude:`) | | `.claude/commands/*.md` | `/.md` (`allowed-tools` moves under `x-claude:`) | | `.claude/settings.json` hooks | `/[-]-.yaml`, such as `pretooluse-bash-exit-0.yaml`, with a `description` (HTTP, MCP-tool, and prompt handlers keep their native fields in separate specs; `target: claude` unless the hook can be shared, see below) | | `.claude/settings.json` `permissions.allow`, `ask`, `deny` | `/permissions-claude.yaml` (a portable settings spec) | | `.claude/settings.json` other non-hook keys | `.agnostic-ai/overlays/claude.settings.json` | | `.mcp.json` (`mcpServers.`) | `/.yaml` (one spec per server) | **Instructions.** A hand-written `CLAUDE.md` imports whole, not split into rules, since each section would then load twice. When `.claude/rules/` exists (even empty), rule files come only from it. Import follows Claude Code's lookup order: `CLAUDE.md`, `.claude/CLAUDE.md`, `AGENTS.md`, `.claude/AGENTS.md`. Only one importer may slice the root `AGENTS.md`, so import skips it when `codex`, `amp`, `warp`, `crush`, `kiro`, or `opencode` imports too. Delete `.claude/CLAUDE.md` after the next sync, as import says, or Claude Code loads both it and the root file. **Nested `CLAUDE.md`.** Sync writes nested rules to `.claude/rules//`, so import tells you to delete each nested `CLAUDE.md` it read. `doctor --fix` deletes one still matching its rule and keeps one you edited. Once `.claude/rules/` holds a directory's scoped rules, `sync` itself deletes a nested `CLAUDE.md` that only imports `@AGENTS.md`. Any other line, or a `sync.unmanaged` entry, keeps the file. `sync --backup` keeps the deleted file as `CLAUDE.md.bak`; `revert` restores it. **Agents, skills, commands.** `allowed-tools` moves under `x-claude:`, where `lint` accepts it and sync writes it back. An agent `model` naming a Claude model (an alias such as `opus` or `fable`, `inherit`, or a `claude-*` id) imports as `model: {claude: }`, so other targets use their default; sync writes `model: ` back. **Settings.** Permission lists become a portable settings spec that `lint` checks and every target gets (with a sync note where unsupported). Import skips rules that another settings spec or `outputs.claude.settings.permissions` declares. Add `target: claude` to keep them Claude-only. The overlay captures every other non-`hooks` key, including `permissions.defaultMode`. Re-import after editing settings.json by hand. Import leaves out a matcher group when one of its hooks holds a credential in its command, arguments, URL, header, `if` rule, status message, prompt, or MCP tool input, or names a Slack or Discord webhook, and prints a warning naming the event and field. The [setting value rule](https://agnostic-ai.org/docs/spec-format/mcp-import/#setting-values) decides which headers and inputs hold a credential. Sync leaves that group in settings.json as written but does not manage it; to manage it, add it as a hook spec under `.agnostic-ai/local/hooks`, which Git ignores. `effortLevel` is the one key import moves out of the overlay. A valid value becomes `effort` in `/claude.yaml`, which every target syncs, unless another settings spec sets a different effort; then it goes under `x-claude.effortLevel`. An invalid value stays in the overlay. ### Unread files and rewritten paths The import summary lists each unread file under `.claude/`, such as `.claude/templates/post.md`. Sync leaves them in place. A skill that reads one still finds it, but other tools get no copy. Imported text naming a Claude path, such as `.claude/skills/style/SKILL.md`, now names its source, `.agnostic-ai/skills/style/SKILL.md`. An `@.claude/rules/.md` line for an imported rule is dropped. Import reports each Claude path it leaves alone. ### Portable imported hooks An imported hook is pinned with `target: claude` unless every other configured target can check it and run it as written. Only Codex can check today. The hook must use an event Codex has, match only tools Codex reports (`Bash`, `apply_patch`, `Edit`, `Write`, `mcp__*`) or a `SessionStart` or compact source, use a command or `mcp_tool` handler, and not set `if`, `shell`, `once`, or `asyncRewake`. So in a `claude,codex` project a `PreToolUse` hook on `Bash` stays portable. With a tool that cannot check, such as `cursor`, every pin stays. The import summary explains each kept pin. For enforcement boundaries, see [protected paths in Claude Code](https://agnostic-ai.org/docs/targets/claude/#protected-paths). # Codex (`codex`) Write project instructions and rules under `.agnostic-ai/`. Sync gives Codex `AGENTS.md`, native skill folders, and any configured agents, hooks, MCP servers, or settings. The same sources can also feed Claude Code. Using this provider through Composer? Follow the [Composer setup guide](https://agnostic-ai.org/docs/composer/). ## First rule Install the client using the [official setup guide](https://learn.chatgpt.com/docs/codex/cli), then open your project in it. Select `codex` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule). The shared commit-prefix rule becomes a section in root `AGENTS.md`. With Claude Code also selected, the same source becomes `.claude/rules/conventional-commits.md`. Write shared project instructions in `.agnostic-ai/AGNOSTIC_AI.md`. Sync combines them with the Codex rules in `AGENTS.md`. Edit those sources for later changes. Already configured? Check [import coverage](https://agnostic-ai.org/docs/targets/codex-reference/#import) before syncing. Import reads root and nested instruction files, agents, skill folders, hooks, MCP servers, environments, and remaining project config into an overlay. ## Output | Source | Default output | |---|---| | Instructions and project-wide rules | `AGENTS.md` | | Directory-scoped rules | `/AGENTS.md` | | Skills and bundled files | `.agents/skills//SKILL.md` | | Commands | `.agents/skills//SKILL.md`, as [skills the user runs by name](https://agnostic-ai.org/docs/spec-format/commands/#tools-without-commands) | | Agents | `.codex/agents/.toml` | | Hooks | `.codex/hooks.json`, when configured | | Settings and MCP servers | `.codex/config.toml`, when configured | | App environment | `.codex/environments/environment.toml`, when configured | | Exec policies | `.codex/rules/default.rules`, opt-in | An empty kind creates no corresponding files. Prefer skills for prompt procedures: project custom-prompt output is a deprecated compatibility option that Codex does not discover. With `outputs.codex.commands-dir` set, commands go there instead of becoming skills. See [output overrides](https://agnostic-ai.org/docs/targets/codex-reference/#config-keys). ## Loading and limits - Codex [discovers instructions from the project root to the session's working directory](https://learn.chatgpt.com/docs/agent-configuration/agents-md). Start inside a subtree to load its nested `AGENTS.md`; editing there from a root-started session does not load it. Start a new session after changing instructions. - Command and MCP-tool hooks require trust through `/hooks`, including after their configuration changes. Project hooks also need a trusted project. `agnostic-ai doctor -t codex` checks saved trust, not whether an already-running session bypasses it. - Agents inherit the session sandbox. Codex v0.155.0 and later ignore an agent's `sandbox_mode`, so `readonly: true` does not enforce read-only behavior there. Use session limits and [exec policies](https://agnostic-ai.org/docs/targets/codex-reference/#codex-exec-policies). Codex [project config takes precedence over user config in a trusted project](https://learn.chatgpt.com/docs/config-file/config-basic). Keep user-level keys in the [home configuration](https://agnostic-ai.org/docs/global-configuration/), and use the [Codex native reference](https://agnostic-ai.org/docs/targets/codex-reference/#codex-config) for supported project fields and overlay precedence. ## Protected paths A settings spec's `protected` block creates an `apply_patch` hook and `.codex/hooks/agnostic-ai-protect.sh`. It checks added, edited, deleted, and moved paths in a patch. `decision: ask` blocks and tells the agent to ask you; the hook cannot open an approval prompt. The guard needs trusted hooks, `sh`, and `awk`. It does not see shell commands that write files. On Windows, if `sh` is missing, the hook cannot start and the edit goes through. See [protected paths](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). ## Verify Run `agnostic-ai sync --check` and inspect root `AGENTS.md` for the commit-prefix rule. Start Codex from the project root and ask for a commit subject for a small fix; it should use `fix:`. For a scoped rule, start a new session inside its directory. `agnostic-ai explain --file services/api/handler.go --target codex` reports planned instruction files and discovery limits; it does not confirm the files loaded in a running session. Next, [add a reusable skill](https://agnostic-ai.org/docs/spec-format/skills/) or compare the same setup in [Claude Code](https://agnostic-ai.org/docs/targets/claude/). # Codex native reference Start with the [Codex setup](https://agnostic-ai.org/docs/targets/codex/). This page covers native mappings and overrides. For shared home configuration and ownership, see [global configuration](https://agnostic-ai.org/docs/global-configuration/). ## Rule placement and discovery Rules without a directory scope go into root `AGENTS.md`. Whole-directory patterns such as `src/api/**` can become nested files. `alwaysApply: true` keeps an unscoped rule at the root. A scope plus patterns covers both: `scope: services/payments` and `globs: tests/payments/**` writes to both directories. Root file selectors, filename filters such as `src/**/*.ts`, and mixed selectors cannot be represented by directory instruction files. Sync keeps them at root with a note, or fails with `on-unsupported: error`. Shared-file conflicts can also force a root fallback. See [scoped context safeguards](https://agnostic-ai.org/docs/scoped-context/#shared-files-and-safe-updates). Codex discovers `AGENTS.md` from the session working directory and its parents. Start in the subtree to load its nested file; a root-started session does not discover it merely by editing there. [Explain a source file](https://agnostic-ai.org/docs/cli-reference/inspect/#explain-a-source-file) reports planned instruction paths and discovery limits. ## Agent mappings | Source | Codex output | |---|---| | Name, description, body | `name`, `description`, `developer_instructions` in `.codex/agents/.toml` | | Per-target `model` | Codex model ID; Claude-only names are omitted with a note | | `effort` | `model_reasoning_effort`; integer budgets are omitted | | `readonly: true` | Legacy `sandbox_mode = "read-only"`; current agents inherit the session sandbox | | `x-codex` model fields | `model`, `model_reasoning_effort`, `model_reasoning_summary`, `model_verbosity` | Sync keeps `features.*`, including false, and `skills.config` path/enabled entries through import. Agent files cannot override the parent's `approval_policy`, `model_provider`, `mcp_servers`, or `service_tier`; preserved import keys get a no-effect note. Set them in session configuration. A generic `tools` list is dropped because Codex's `tools` is a table. Codex v0.155.0 and later ignore agent `sandbox_mode`. The legacy key remains for older releases; an `x-codex.sandbox_mode` override wins, and null omits it. Use session limits or exec policies for enforced restrictions. ## Skill mappings Skills go to `.agents/skills//SKILL.md`, with required `name` and `description`. A scoped skill goes under `/.agents/skills/`. Codex scans from its working directory up to the repository root. Claude's dynamic command syntax and `$ARGUMENTS` remain plain text, with a note. Skill `model` and `effort` have no native field and are omitted. `disable-model-invocation: true` becomes `policy.allow_implicit_invocation: false` in `agents/openai.yaml`. An explicit policy value in `x-codex` or a bundled YAML file wins. Sync writes or merges `agents/openai.yaml` for that policy or `x-codex.interface`, `policy`, and `dependencies`. Tools sharing the same skill directory get the merged file; separate tool directories keep the bundled version. ## Hook mappings and trust Hooks go to `.codex/hooks.json`, grouped by event and matcher. [Trust handlers with `/hooks`](https://learn.chatgpt.com/docs/hooks) after sync, and again after their configuration changes. Project hooks also need a trusted project. `agnostic-ai doctor -t codex` checks saved trust in `CODEX_HOME` or `~/.codex`; a session trust bypass is outside that check. Command hooks keep `timeout`, `statusMessage`, `commandWindows`, `additionalContextLimit` (including zero), and `async`. MCP-tool hooks use `type: mcp_tool`, required `server` and `tool`, optional `input`, `timeout`, and `statusMessage`. See the [native hooks reference](https://learn.chatgpt.com/docs/hooks) for event names and payloads. `PreModelRequest` and `PostModelRequest` are observer events in Codex 0.164.0. Their output is logged; it cannot block or change a model request. Omit `matcher`, since these events supply no matcher input ([released source](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/hooks/src/events/model_request.rs)). Sync prefixes shell commands with `export AGNOSTIC_AI_TARGET=codex; `. This requires a POSIX session shell on macOS and Linux. On Windows it uses `commandWindows`, or copies the declared command without that prefix. `args` fold into POSIX-quoted shell arguments; supply `commandWindows` when Windows quoting differs. `Edit` and `Write` aliases report the tool as `apply_patch`. Edited paths live inside `tool_input.command`, not `tool_input.file_path`. Use [`agnostic-ai hook paths`](https://agnostic-ai.org/docs/spec-format/hook-reference/#edited-paths). Sync detects inline reads of the missing field, but does not inspect your scripts. Imported `$CLAUDE_PROJECT_DIR` paths use the Git worktree root and need Git and a POSIX shell. Unsupported forms get a note or fail under `on-unsupported: error`. Import also reads hooks in `config.toml`, deduplicating them with JSON hooks; JSON wins. ## Reviews and environments Reviews become `## Code Review Rules` in root or scoped `AGENTS.md`. Other tools reading that file see the review section too. A legacy `rules-file` override prevents root reviews and raises a note. Environment specs write the Codex app's `.codex/environments/environment.toml`. `setup`, `cleanup`, and `setup-windows` become native scripts; `dev-commands` become action buttons with name, command, and icon (default `run`). `cwd` prefixes the command with `cd`. Last top-level values win. Ports, environment maps, URLs, install, and terminals have no native key and get a note. An entry with a `url` and no `command` has nothing to run, so sync skips it with a note and lint reports it (LINT016). ## MCP mappings Servers become `[mcp_servers.]` in project config. `disabled: true` becomes `enabled = false`. | Transport | Native fields | |---|---| | Stdio | `command`, `args`, `env`, `cwd`, `env_vars` | | HTTP/SSE | `url`, `bearer_token_env_var`, `http_headers`, `env_http_headers`, `auth`, `http_headers_helper` | | Either | `enabled_tools`, `disabled_tools`, `required`, startup and tool timeouts, `default_tools_approval_mode`, `experimental_environment` | `${NAME}` in env or headers becomes a native environment reference. A reference in URL or args leaves the server out with a note. See [environment references](https://agnostic-ai.org/docs/spec-format/mcps/#environment-references). HTTP OAuth supports `scopes`, `oauth_resource`, and an `oauth` table with `client_id`, `callback_url`, and `callback_port`. A per-tool `tools` map writes native sub-tables. Set only one startup timeout unit (`startup_timeout_sec` or `startup_timeout_ms`). See the [Codex config reference](https://learn.chatgpt.com/docs/config-file/config-reference) for native options and defaults. Sync rebuilds project `config.toml`. Import existing keys into the overlay before syncing; keep unmanaged user configuration in your home config. ## Config keys | Key | Default | Notes | |---|---|---| | `outputs.codex.agents-dir` | `.codex/agents` | override the agent output directory | | `outputs.codex.skills-dir` | `.agents/skills` | the path Codex scans | | `outputs.codex.nested-glob-rules` | `true` | put rules that cover whole directories in nested `AGENTS.md`; `false` keeps them in the root file | | `outputs.codex.shared-subagents` | `true` | writes the per-skill tree at `skills-dir`; set `false` to skip Codex skills | | `outputs.codex.commands-dir` | unset | set to e.g. `.codex/prompts` to write the deprecated project prompts layout | | `outputs.codex.mcp-file` | `.codex/config.toml` | | | `outputs.codex.hooks-file` | `.codex/hooks.json` | | | `outputs.codex.environment-file` | `.codex/environments/environment.toml` | | | `outputs.codex.rules-file` | unset | writes legacy concatenated rules and skips the write of instructions that point to source specs | | `outputs.codex.exec-policies` / `outputs.codex.exec-policies-file` | unset | write `.codex/rules/default.rules` | | `outputs.codex.exec-policies-from-permissions` | `false` | translate simple Bash permission rules when no native policy source is set | ## Codex config `outputs.codex.config` sets the listed `.codex/config.toml` keys on each sync. It wins over a portable Settings spec `model`. In a trusted project, [project values take precedence over home values](https://learn.chatgpt.com/docs/config-file/config-basic). Keep user-level keys in your [home configuration](https://agnostic-ai.org/docs/global-configuration/). ```yaml outputs: codex: config: model: gpt-6-luna sandbox: workspace-write approval-policy: on-request model-reasoning-effort: high model-reasoning-summary: auto history-persistence: save-all ``` | Field | Type | Notes | |-------|------|-------| | `model` | string | Model identifier Codex uses for this project. | | `sandbox` | string | Written as `sandbox_mode`: `read-only`, `workspace-write`, or `danger-full-access`. Any other value stops sync. | | `approval-policy` | string | `on-request` for interactive approvals or `never` to reject approval prompts. `on-failure` is deprecated; `untrusted` is unsupported ([config reference](https://learn.chatgpt.com/docs/config-file/config-reference)). | | `model-reasoning-effort` | string | Passed through unchanged. Use an effort the selected model and client advertise, such as `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`. | | `model-reasoning-summary` | string | Reasoning summary verbosity: `auto`, `concise`, `detailed`. | | `history-persistence` | string | Session transcript persistence: `save-all` or `none` ([config reference](https://learn.chatgpt.com/docs/config-file/config-reference)). | | `notify` | string array | Not written. Codex ignores `notify` in a project config; set it in `~/.codex/config.toml`. | | `profiles` | map | Not written. Codex 0.134.0 and later read no `[profiles.*]` table from any `config.toml`. Put each profile in `~/.codex/.config.toml` and select it with `--profile `. | | `model-providers` | map | Not written. Codex ignores `[model_providers.*]` in a project config; set them in `~/.codex/config.toml`. | Sync writes the overlay that `import codex` captured, `.agnostic-ai/overlays/codex.config.toml`, before the `[mcp_servers.*]` sections built from your specs. Captured configuration can be regenerated from that overlay, except for the project-ignored keys below. ### Keys Codex ignores in a project config Codex drops these top-level keys from a project `.codex/config.toml`, with a startup warning ([docs](https://learn.chatgpt.com/docs/config-file/config-advanced), [source](https://github.com/openai/codex/blob/0b1b78a4f1694e2b9e393d385c7b82ca714ca08a/codex-rs/config/src/loader/mod.rs#L88-L101)): `openai_base_url`, `chatgpt_base_url`, `apps_mcp_product_sku`, `model_provider`, `model_providers`, `notify`, `profile`, `profiles`, `experimental_realtime_ws_base_url`, `otel`, `responses_api_metadata`, and `experimental_realtime_webrtc_call_base_url`. Sync writes none of them. Keep user-level keys in `~/.codex/config.toml`; profiles use the separate `~/.codex/.config.toml` files described above. The `notify`, `profiles`, and `model-providers` fields above print a note. When the overlay sets one, sync leaves it out of `.codex/config.toml`, keeps the overlay file, and prints a note naming the key. These notes never fail the sync, even with `on-unsupported: error`. How sync resolves conflicts: - `model` wins in this order, lowest first: portable Settings spec, `outputs.codex.config.model`, overlay. Either of the last two replaces the settings model, so a Claude model name in a settings spec raises no note. - The overlay wins any other conflict with `outputs.codex.config.*`, and the lower value is dropped. - On import, a top-level `model_reasoning_effort` moves to `effort` in `/codex.yaml` when no settings spec sets `effort`, so every target syncs it. `[profiles.*]` values, and a value another settings spec shadows, stay in the overlay. - On import, a value that holds a credential, such as a `[model_providers.*]` bearer token, an `http_headers` key, or a `shell_environment_policy.set` token, is left out of the overlay with a warning naming its key, by the [setting value rule](https://agnostic-ai.org/docs/spec-format/mcp-import/#setting-values). Keys whose names start with `env_` hold variable names and stay. The overlay is then written again without its comments. Keep such values in `~/.codex/config.toml`. ### Codex exec-policies `outputs.codex.exec-policies` (list) or `outputs.codex.exec-policies-file` (path to a YAML list) declares Codex's Starlark exec-policy rules for `.codex/rules/default.rules`. Each entry allows, forbids, or prompts for a shell command prefix. ```yaml outputs: codex: exec-policies: - pattern: ["composer", "test"] decision: allow # allow | forbidden | prompt justification: Composer scripts are project entrypoints. match: ["composer test", "composer test -- --filter Foo"] - pattern: ["rm", "-rf", "/"] decision: forbidden justification: Never remove the filesystem root. ``` | Field | Required | Notes | |-------|----------|-------| | `pattern` | yes | Shell command prefix tokens (`["composer", "test"]`). Becomes the `prefix_rule(pattern = [...])` argument. | | `decision` | yes | One of `allow`, `forbidden`, `prompt`. | | `justification` | no | Human-readable reason passed to Codex as `justification`. | | `match` | no | Example command strings passed to Codex as `match`; Codex validates them when loading the policy. | For many policies, use a file: `exec-policies-file: ./.agnostic-ai/codex.exec-policies.yaml`. Inline entries come first. [Codex applies the strictest matching decision](https://learn.chatgpt.com/docs/agent-configuration/rules) (`forbidden`, then `prompt`, then `allow`), so order never matters. `import codex` captures every `prefix_rule(...)` in `.codex/rules/default.rules` into `.agnostic-ai/overlays/codex.exec-policies.yaml`, skipping a file sync generated. Sync uses that overlay when neither an inline list nor `exec-policies-file` is set. ### Translate Bash permissions Set `outputs.codex.exec-policies-from-permissions: true` to generate command prefixes from portable Settings specs that target Codex and from `outputs.claude.settings.permissions`. The lists combine in source order, with duplicates removed. Hand-written Claude settings and user policy files are not read. ```yaml targets: [claude, codex] outputs: claude: settings: permissions: allow: - Bash(npm run check:*) - Bash(npx vitest run:*) - Bash(git diff:*) codex: exec-policies-from-permissions: true ``` This writes three `prefix_rule` entries. `Bash(a b c *)` and `Bash(a b c:*)` become `pattern = ["a", "b", "c"]`. `allow`, `deny`, and `ask` become `allow`, `forbidden`, and `prompt`. Codex has no exact-match rule: a prefix matches extra arguments, even from a bare rule without `:*`. Claude Code allows `Bash(git push)` only as a bare `git push`, but a `git push` prefix rule would allow extra flags too. Project rules load only in a trusted project. Sync leaves out an exact `allow` rule that Codex would widen, and names it with its source: ```text note: codex: agnostic-ai.yaml: permissions.allow rule Bash(git push) is not written: Codex has no exact-match rule, and a `git push` prefix rule also allows extra arguments; write Bash(git push:*) to allow them, or use outputs.codex.exec-policies ``` The rule is written when a deny or ask rule on the same or a shorter prefix already decides every command it would match, or when a wildcard `allow` such as `Bash(git:*)` already allows the extra arguments. `on-unsupported: error` fails on a left-out rule; `silent` omits the note. Exact `deny` and `ask` rules are always written. Only plain, unquoted words translate. A Bash rule with quotes, escapes, a `*` other than one trailing ` *` or `:*`, shell operators, expansions, assignments, or shell keywords gets a sync note. `on-unsupported: error` fails on it; `silent` omits it. Rules for other tools, such as `Read(.env)` or `WebFetch`, share one `permissions` sync note and never fail the sync. Use explicit `exec-policies` for a command that cannot translate. An inline policy list (even `exec-policies: []`), `exec-policies-file` (even an empty file), or imported policy overlay wins. Sync skips translation, notes which source won, and never modifies the policy file. `lint` warns with LINT021 when a supported Bash `allow` or `deny` rule, including declared portable deny and ask exclusions, has no native prefix that covers it with the same decision. Broader native prefixes count. Restrictive descendants of an allowed prefix also warn. It checks declared prefixes only. `lint --strict` fails on the warning. ## Import `agnostic-ai import codex` finds `AGENTS.md` at any depth and reads the other Codex files: | Source | Becomes | |--------|---------| | `AGENTS.md` at the root | `.agnostic-ai/AGNOSTIC_AI.md`; the rules block `sync` appends becomes rules | | `/AGENTS.md` (nested, hand-written) | one rule with the whole file, named after the scope: `api.md` for `services/api/`, or `services-api.md` when another scope also ends in `api`, with inferred `globs: /**` | | `/AGENTS.md` (nested, with `import.codex.shred: true`, or written by `sync`) | one rule per section (`api-tests.md` for `## Tests` in `services/api/`); text above the first `##`, past the title, becomes one more rule named after the scope | | `## Code Review Rules` or `## Review guidelines` in a nested `AGENTS.md`, or the review section `sync` writes to any `AGENTS.md` | `/.md` with `scope: ` (`review.md` at the root), not a rule | | `## Conventions` / `## Agents` / `## Skills` wrapper sections | unwrapped: their `### children` become the rules | | Single-line italic (`_text_`) under a rule heading | moved into the rule's `description` | | `.codex/agents/*.toml` and `.agents/agents/*.toml` | `/.md`. When the agent spec already exists, as after `import claude`, the Codex `model` lands as `model: {codex: }` (or a `codex` entry in an existing per-target map), so Claude Code keeps its own default. A differing shared scalar `model` gets `x-codex.model` | | `.agents/skills//SKILL.md` (+ hand-written `agents/openai.yaml`, asset folders) | `//SKILL.md` (+ nested assets, exec bits preserved). Assets with the generated header are skipped; sync renders its own `agents/openai.yaml` from the skill fields | | `.codex/hooks.json` and inline hooks in `.codex/config.toml` | `/-.yaml` (one spec per handler). A duplicate hook in both files imports once; the JSON definition wins | | `.codex/config.toml` `[mcp_servers.]` | `/.yaml` | | `.codex/config.toml` remaining keys (model, sandbox, approval_policy, `[history]`, ...) | `.agnostic-ai/overlays/codex.config.toml` (`hooks` + `mcp_servers` stripped). The [ignored keys](#keys-codex-ignores-in-a-project-config) stay in the overlay but are left out of `.codex/config.toml` on sync | | `.codex/prompts/*.md` | `/.md` (unchanged copy) | | `.codex/environments/environment.toml` | `/codex.yaml`: `[setup]`, `[setup.win32]`, `[cleanup]`, and `[[actions]]` become `setup`, `setup-windows`, `cleanup`, and `dev-commands`. A file with a `[setup.darwin]` script, an action `platform`, or another key stays as written with a note | Two sections with the same heading in one file get separate names (`style.md`, `style-2.md`). The walk skips hidden directories, the configured source directories, `node_modules/`, `vendor/`, directories git ignores, and directories with their own `.git`. See [Codex config](#codex-config) for overlay precedence and project-ignored keys. For enforcement boundaries, see [protected paths in Codex](https://agnostic-ai.org/docs/targets/codex/#protected-paths). # Gemini CLI (`gemini`) agnostic-ai writes `GEMINI.md`, native subagents, commands, and skills under `.gemini/`, and merges MCP, hooks, and settings into `.gemini/settings.json`. Gemini CLI's own docs say Antigravity CLI replaced it for unpaid-tier and Google One users on June 18, 2026. Gemini CLI still ships and this target is unchanged. To write for Antigravity, add the [`antigravity`](https://agnostic-ai.org/docs/targets/antigravity/) target. ## First rule Install the client using the [official setup guide](https://geminicli.com/docs/), then open your project in it. Select `gemini` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `GEMINI.md`. Directory-specific rules use nested `GEMINI.md` files. See [scoped context](https://agnostic-ai.org/docs/scoped-context/) before sharing file-pattern rules. For existing files, check [what `import gemini` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `GEMINI.md` for the commit-prefix rule. Start Gemini CLI from the project root and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` GEMINI.md # entry-point pointer body (written by sync) .gemini/agents/.md # one subagent per agent .gemini/commands/.toml # one per command .gemini/skills//SKILL.md # one folder per skill, bundled assets included .gemini/commands/skill-.toml # extra command form, only when emit-skills-as-commands: true .gemini/settings.json # when MCP, hook, or settings entries exist (merged with user config) .geminiignore # when ignore entries exist ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Rules Rules without a directory scope go into the root `GEMINI.md`. `scope: services/payments` sends a rule to `services/payments/GEMINI.md` instead, and `globs: tests/payments/**` adds it to `tests/payments/GEMINI.md`. `globs` alone creates no directory scope. Directory documents cannot hold external file filters or root selectors with a scope, so those follow `on-unsupported`. Scoped rules need any legacy `outputs.gemini.rules-file` override removed. See [scoped context](https://agnostic-ai.org/docs/scoped-context/) for selector and runtime limits. ### Agents Native [subagents](https://geminicli.com/docs/core/subagents.md) in the project directory Gemini CLI scans. They get automatic delegation, their own context window, `@name` invocation, and a `/agents` listing. Frontmatter carries the required `name` and `description` (default: spec name), plus `kind`, `model`, `temperature`, `max_turns`, and `timeout_mins` when set. The portable `maxTurns` is written as `max_turns`, and a `max_turns` the spec sets wins. The body is the system prompt. - Per-agent MCP servers have no agnostic-ai field. Set `x-gemini.mcp_servers`, with snake_case keys such as `http_url` and `include_tools`. Gemini rejects the documented `mcpServers` key and skips the agent, so sync renames `x-gemini.mcpServers` to `mcp_servers` with a note. - A generic `tools` list maps to [Gemini's tool names](https://geminicli.com/docs/reference/tools): `Read`: `read_file`, `Write`: `write_file`, `Edit`: `replace`, `Glob`: `glob`, `Grep`: `grep_search`, `Bash`: `run_shell_command`, `WebFetch`: `web_fetch`, `WebSearch`: `google_web_search`. Other names drop with a sync note, because an unknown entry would limit the subagent to a tool that does not exist. Omit `tools` to inherit all tools. - `x-gemini.tools` replaces the translated list with Gemini's own names, including the wildcards `*`, `mcp_*`, and `mcp__*`. - `outputs.gemini.emit-agents-as-commands: true` also writes each agent as a `.toml` slash command, so you can type `/name`. With it off, sync removes earlier generated TOML files. ### Skills Native [Agent Skills](https://geminicli.com/docs/cli/skills/) at `.gemini/skills//SKILL.md`, with bundled files copied as they are. Gemini CLI also reads `.agents/skills/`, which wins when a name appears in both. `outputs.gemini.emit-skills-as-commands: true` adds one `skill-.toml` command per skill. ### Commands `.gemini/commands/.toml`, the [project slash-command directory](https://geminicli.com/docs/cli/custom-commands.md). `description` becomes the TOML `description` and the body becomes `prompt`. A command and an agent can share a name. A command named `git:commit` is written as `.gemini/commands/git/commit.toml`, which Gemini runs as `/git:commit`. An agent keeps its name as one file, `.gemini/agents/.md`, so on Windows sync refuses an agent named with a colon; see [path semantics](https://agnostic-ai.org/docs/configuration/#path-semantics). ### Settings The portable `model` maps to `model.name`. Other model options and settings stay. `x-gemini` keys merge in. Portable permission rules raise a sync note. At user scope, portable [voice settings](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) write the experimental `experimental.voiceMode` and `experimental.voice.activationMode` keys; Gemini has no dictation language key. ### MCP The `mcpServers` map. Streamable-HTTP servers (`type: http`) use `httpUrl`. SSE servers (`type: sse`) use `url`. Stdio servers also accept `cwd`. `timeout` (milliseconds), `trust` (skip tool-call confirmations), `description`, `includeTools`, and `excludeTools` are copied as written ([configuration reference](https://geminicli.com/docs/reference/configuration.md)). Import reads `httpUrl` back as `type: http` and a bare `url` as `type: sse`. Native OAuth options pass through `x-gemini.oauth`, including `enabled`, `allowPrivateNetwork`, and `authorizationResponseIssParameterSupported`. Import keeps this object Gemini-specific and leaves out servers with literal OAuth credentials; existing credential references remain references. ### Hooks The `hooks` map, keyed by event. Gemini documents 11 events: `BeforeTool`, `AfterTool`, `BeforeAgent`, `AfterAgent`, `Notification`, `SessionStart`, `SessionEnd`, `PreCompress`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`. Each hook has a `matcher` and a nested `hooks` array of `{type: "command", command}` handlers ([hook reference](https://geminicli.com/docs/hooks/reference/)). With `builtins: [memory]`, a `SessionStart` hook adds the [shared memory](https://agnostic-ai.org/docs/memory/) index to the session. - Timeouts convert from seconds to milliseconds (vendor default 60000 ms). - A `command` list becomes separate handlers in one hook. `x-gemini.sequential: true` runs them in order. - `description` is copied to each handler, `x-gemini.name` sets its display name, and `x-gemini.env` sets its environment variables. Your existing keys stay. - Gemini runs `command` through `bash -c` (PowerShell on Windows) and has no `args` field. Sync folds `args` into `command`, each single-quoted (`node 'guard.js'`). `import gemini` reads it back as one shell-form `command`. - [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) tests a `BeforeTool`, `AfterTool`, `BeforeAgent`, or `SessionStart` hook before a session using the same behavior. ### Ignore Ignore specs are joined into `.geminiignore` (gitignore syntax), the file [Gemini CLI reads](https://geminicli.com/docs/cli/gemini-ignore/). Override with `outputs.gemini.ignore-file`. ### Folded args edge cases PowerShell reads folded args the same way unless one holds an apostrophe. A quoted command path with a space runs in bash but not PowerShell, so use a shell-form `command` there. Gemini replaces `$GEMINI_*` path variables and `$CLAUDE_PROJECT_DIR` with its own quoted path, even inside those quotes. Such an argument splits when the project path has a space. ### Old .aiexclude files Older versions wrote `.aiexclude`, which Gemini CLI never reads. Sync removes a generated one. ## Shared memory Gemini CLI saves facts by editing its memory files itself, and has no setting to turn that off. On its own, it saves team facts to `GEMINI.md`, which sync writes over. With `builtins: [memory]`, sync adds the `gemini-memory-tiers` rule to `GEMINI.md`. Gemini CLI keeps its own memory, with one change for the file sync owns: | Gemini CLI tier | Saves to | | --- | --- | | Project instructions: team conventions, architecture rules, workflows | `.agnostic-ai/memory/`, after you confirm, instead of `GEMINI.md` | | Private project memory: your machine and local setup | its own private memory (`~/.gemini/tmp//memory/`), plus a copy in your personal memory folder | | Global personal memory: preferences for every project | `~/.gemini/GEMINI.md`, outside the block `sync --global` writes, plus a copy in your personal memory folder | The copies are how other tools read what Gemini CLI learns. If it edits `GEMINI.md` anyway, the next sync keeps the edit in `GEMINI.md.bak` and suggests saving it as a fact in `.agnostic-ai/memory/`. When Gemini CLI's instructions file is one another target also reads, such as `AGENTS.md`, sync leaves the rule out of that file and prints a note. `sync --global` leaves it out too, because the global block reaches every tool. See [shared memory](https://agnostic-ai.org/docs/memory/). ## Config keys | Key | Default | Notes | |---|---|---| | `outputs.gemini.agents-dir` | `.gemini/agents` | | | `outputs.gemini.commands-dir` | `.gemini/commands` | | | `outputs.gemini.skills-dir` | `.gemini/skills` | | | `outputs.gemini.mcp-file` | `.gemini/settings.json` | also holds hooks and settings | | `outputs.gemini.emit-skills-as-commands` | `false` | | | `outputs.gemini.emit-agents-as-commands` | `false` | | | `outputs.gemini.rules-file` | unset | writes legacy joined rules and skips the write of instructions that point to source specs | | `outputs.gemini.ignore-file` | `.geminiignore` | | ## Import `import gemini` reads `.gemini/agents/*.md`, `.gemini/commands/**/*.toml`, `.gemini/skills//`, and the MCP, hooks, and default model in `.gemini/settings.json`. - **Instructions**: the root `GEMINI.md` goes to `.agnostic-ai/AGNOSTIC_AI.md`. Only the rules block that `sync` appends becomes rules. - **Nested files**: a hand-written `/GEMINI.md` becomes one rule with the whole file and `scope: `, named after the scope: `api.md` for `services/api/`, or `services-api.md` when another scope also ends in `api`. A directory with one rule syncs back unchanged. A nested file that `sync` wrote splits back into its rules. - **Commands**: `prompt` becomes the body in either documented form (triple-quoted block or single-line string). `description` stays in frontmatter. A file in a subfolder, such as `git/commit.toml`, imports as the command `git:commit`. This also covers older agent-as-command files and the `emit-agents-as-commands` and `emit-skills-as-commands` copies, which sync back unchanged. - **Hooks**: nested hooks keep commands, matchers, names, descriptions, environment maps, timeouts, and sequential groups. Hooks with the same command and matcher import separately. A single handler's whole-second timeout imports in seconds. Otherwise `x-gemini.timeout` keeps the native milliseconds. Multiple handlers stay together under `x-gemini.hooks`, which replaces `command` for Gemini. Old flat hook files still import. ## Protected paths Enforced (hook). Gemini CLI's policy engine can deny `write_file` and `replace`, but its workspace tier "is currently non-functional" ([policy engine](https://geminicli.com/docs/reference/policy-engine/)). So sync writes `.gemini/hooks/agnostic-ai-protect.sh` and a `BeforeTool` hook on `^(write_file|replace)$` in `.gemini/settings.json`. For a protected `tool_input.file_path`, the script exits 2 with the reason on stderr. Gemini CLI then blocks the call and shows the reason ([hooks reference](https://geminicli.com/docs/hooks/reference/)). The script needs only `sh` and `awk`. `decision: ask` also blocks and tells the agent to ask. Gemini CLI runs project hooks only in a trusted folder and reads `.gemini/settings.json` from the directory you start in, so start at the project root. The hook does not catch shell commands that write files. ### Script failures and odd paths Gemini CLI lets an edit through on exit 1 or a timeout, so the script blocks when it cannot run: no `awk`, an unreadable project root, or an `awk` failure. It ignores case, reads `\` as a path separator, and checks a path with a leading `@` both ways, because Gemini CLI strips the `@`. `replace` searches the workspace for a relative path that is missing from the project root, so the hook blocks that call and asks for the full path. On Windows it needs `sh` on `PATH` (Git for Windows has one), or the edit goes through. See [Protected paths](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Qwen Code (`qwen`) agnostic-ai writes Qwen Code's native project files and imports existing ones. This target follows [Qwen Code v0.26.0](https://github.com/QwenLM/qwen-code/releases/tag/v0.26.0). ## First rule Select the target in `agnostic-ai.yaml`: ```yaml targets: [qwen] ``` Save `.agnostic-ai/rules/commit-prefix.md`: ```markdown --- name: commit-prefix --- Start commit subjects with fix:, feat:, docs:, or chore:. ``` Run: ```bash agnostic-ai sync -t qwen agnostic-ai sync --check -t qwen ``` Inspect `QWEN.md` for the rule. This checks generated files. To check discovery, start Qwen Code from the project root and ask for a commit subject. For an existing setup, [import it](#import) before syncing. ## Output ```text QWEN.md # root instructions and rules /QWEN.md # directory-scoped rules .qwen/rules/.md # rules with file patterns .qwen/agents/.md # native subagents .qwen/skills//SKILL.md # skills, with bundled files .qwen/commands/.md # slash commands .qwen/settings.json # MCP, hooks, and settings .qwenignore # ignore patterns ``` ## Native mappings Project-wide rules go into `QWEN.md`, and `scope` rules go into `/QWEN.md`. A rule with `globs` or `paths` and no `scope` goes to `.qwen/rules/.md` with a `paths:` list. Qwen leaves that rule out of the prompt until a tool reads or edits a matching file. `alwaysApply: true` keeps a rule in `QWEN.md`. See [scoped context](https://agnostic-ai.org/docs/scoped-context/#qwen-code-rules-with-globs-alone) for the limits. Agents get YAML frontmatter and a prompt body. Portable tool names map to Qwen names; use `x-qwen.tools` for an explicit native list. Native agent fields such as `approvalMode` and record-valued `mcpServers` belong under `x-qwen`. Skills keep their bundled files. Set `x-qwen.model` on a skill to select its native model; the shared skill `model` field is not promoted. Commands use Markdown with optional `description` frontmatter. Qwen interprets `{% raw %}{{args}}{% endraw %}`, `!{shell}`, and `@{path}` in command bodies; sync keeps that text as written. Nested command paths become colon-separated command names, and sync writes a command named `git:commit` back as `.qwen/commands/git/commit.md`, on Windows too. On Windows, a target that writes such a name as one file refuses it; see [path semantics](https://agnostic-ai.org/docs/configuration/#path-semantics). MCP servers share `settings.json` under `mcpServers`. Stdio uses `command`, `args`, `env`, and optional `cwd`; HTTP uses `httpUrl`, and SSE uses `url`. MCP timeouts are milliseconds. Servers marked `disabled: true` are omitted from this output because Qwen ignores that per-server field. A same-name server in existing or inherited configuration can still load; use native `mcp.excluded` to block it. Environment placeholders stay literal until Qwen resolves them. See the [native MCP reference](https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/). Hooks share `settings.json` under `hooks`. Portable `on`, `match`, and `decision: stdout` map to Qwen events, tool names, and blocking decisions. Hook timeouts are seconds. Sync quotes portable arguments into the shell command because Qwen has no separate `args` field. Portable hooks that exit 2 block pre-tool, prompt-submit, and stop events; after-tool and after-edit stop further execution. Session-start and session-end keep their native lifecycle behavior. Project import preserves native command, HTTP, and prompt handlers and their metadata. Settings map portable `model` to `model.name` and permission lists to `permissions.allow`, `permissions.ask`, and `permissions.deny`. Keys under `x-qwen` keep their native spelling. Native `x-qwen.mcpServers` records override portable servers with the same name. Native and portable hook entries are combined. Sync preserves unrelated settings, including the active `disableAllHooks` setting. Legacy `hooks.enabled`, `hooks.disabled`, and `hooks.notifications` metadata is preserved but does not disable hooks in this release. See [Qwen settings](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/). Save `.agnostic-ai/mcps/docs-search.yaml` to use your own HTTP endpoint: ```yaml name: docs-search type: http url: https://mcp.example.com/mcp ``` Replace the example URL with your server's endpoint. After sync, `.qwen/settings.json` contains `mcpServers.docs-search.httpUrl` with that URL. To exclude a server inherited from user or extension configuration, put this in a settings spec: ```yaml x-qwen: mcp: excluded: [server-name] ``` Qwen combines this native exclusion list across trusted settings scopes. Project and global sync merge entries individually, so removing a source exclusion keeps user entries and other MCP settings. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). ## Limits and trust - Portable agent effort and agent permission mode have no equivalent mapping. Use explicit native approval fields under `x-qwen` when needed. Portable lists of agent MCP server names cannot replace Qwen's record of server definitions. - Project hooks and `.qwen/rules/` require a trusted folder. Safe mode and bare mode can disable project customizations. Trust and approval remain Qwen decisions. - `.qwenignore` applies to tools that honor it; it does not prevent every form of file access. - Commands and ignore files sync only at project scope. Global hook specs emit command handlers. - Reviews and environment specs have no native output. ## Global configuration Portable [voice settings](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) write `general.voice.enabled`, `general.voice.mode`, and `general.voice.language`; choose a native `voiceModel` before using dictation. `agnostic-ai sync --global -t qwen` writes instructions, agents, skills, MCP, hooks, and settings under `~/.qwen/`. Set `QWEN_HOME` to replace that whole directory. `QWEN_RUNTIME_DIR` moves runtime state, not configuration. `agnostic-ai import --global qwen` reads the user model, permission lists, and MCP servers. See [global configuration](https://agnostic-ai.org/docs/global-configuration/). ## Config keys | Key | Default | |---|---| | `outputs.qwen.rules-file` | unset, uses instruction files | | `outputs.qwen.rules-dir` | `.qwen/rules` | | `outputs.qwen.agents-dir` | `.qwen/agents` | | `outputs.qwen.skills-dir` | `.qwen/skills` | | `outputs.qwen.commands-dir` | `.qwen/commands` | | `outputs.qwen.conf-file` | `.qwen/settings.json` | | `outputs.qwen.mcp-file` | `.qwen/settings.json` | | `outputs.qwen.hooks-file` | `.qwen/settings.json` | | `outputs.qwen.ignore-file` | `.qwenignore` | Root instructions use the shared [entry-point configuration](https://agnostic-ai.org/docs/configuration/#outputs). Changing a path does not change where Qwen looks for it. Keep overrides aligned with your native configuration. ## Import ```bash agnostic-ai import qwen agnostic-ai lint agnostic-ai sync -t qwen agnostic-ai sync --check -t qwen ``` Import reads root and scoped instructions, rule files directly under `.qwen/rules/`, agents, skill folders and resources, Markdown and legacy TOML commands, `.qwenignore`, and settings, hooks, and MCP servers in `.qwen/settings.json`. A rule file's `paths:` becomes portable `globs`. Import names each rule file without `paths:`: sync moves that rule into `QWEN.md`, so delete the original file. It also names and skips rule files in subfolders, and stops when a rule file shares its name with a rule in `QWEN.md`. Native fields stay under `x-qwen`; HTTP `httpUrl` becomes portable `type: http`, while SSE `url` becomes `type: sse`. Imported legacy commands emit as Markdown. Import rejects credentials in retained agent and skill headers, hooks, and settings. Settings can refer to environment variables with `$NAME` or `${NAME}`. Qwen does not expand those references in an agent's or skill's credential fields, so import rejects them there. # Pi (`pi`) agnostic-ai writes Pi's native project files and imports existing ones. This target follows [Pi v1.4.0](https://github.com/earendil-works/pi/releases/tag/v1.4.0), published as `@earendil-works/pi-coding-agent`. ## First rule Select the target in `agnostic-ai.yaml`: ```yaml targets: [pi] ``` Save `.agnostic-ai/rules/commit-prefix.md`: ```markdown --- name: commit-prefix --- Start commit subjects with fix:, feat:, docs:, or chore:. ``` Run: ```bash agnostic-ai sync -t pi agnostic-ai sync --check -t pi ``` Inspect `AGENTS.md` for the rule. This checks generated files. To check discovery, start Pi from the project root and ask for a commit subject. For an existing setup, [import it](#import) before syncing. ## Output ```text AGENTS.md # root instructions and rules /AGENTS.md # directory-scoped rules .pi/skills//SKILL.md # skills, with bundled files .pi/prompts/.md # slash commands as prompt templates .pi/mcp.json # MCP servers .pi/settings.json # model and thinking settings ``` ## Native mappings Pi reads instruction files along the path from the filesystem root to its current directory. A scoped rule at `services/api/AGENTS.md` reaches a session started there. Reading a file in `services/api/` from a root session does not load that descendant instruction file. Within one directory, Pi selects the first readable file from `AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, and `CLAUDE.MD`. See [Pi configuration](https://github.com/earendil-works/pi/blob/v1.4.0/packages/coding-agent/docs/configuration.md). Skills keep their `SKILL.md` metadata and bundled files in `.pi/skills/`. Pi also discovers shared `.agents/skills/` directories. It keeps the first skill for a duplicate name, so a user-level skill can win over the project copy. Commands become prompt templates in `.pi/prompts/`, with `description` and `argument-hint` frontmatter. Pi expands `$ARGUMENTS`, `$@`, and positional arguments in their bodies. Sync keeps that text as written. See [prompt templates](https://github.com/earendil-works/pi/blob/v1.4.0/packages/coding-agent/docs/prompt-templates.md). MCP entries live under `mcpServers` in `.pi/mcp.json`. Sync accepts a leading UTF-8 byte order mark (BOM) in native MCP files and keeps it when updating the file. Stdio uses `command`, `args`, `env`, and optional `cwd`; HTTP uses `url` and optional `headers`. Portable `disabled: true` becomes native `enabled: false`. Timeouts are seconds. Environment placeholders stay literal. Target-native extras belong under `x-pi`. The order of `toolExposure` patterns follows source order; exact tool names take priority over patterns. See [Pi MCP](https://github.com/earendil-works/pi/blob/v1.4.0/packages/coding-agent/docs/mcp.md). Settings map portable `model` to `defaultModel` and `effort` to `defaultThinkingLevel`. Pi accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Choose a provider separately with native `x-pi.defaultProvider`; sync does not split model IDs. Unrelated settings stay in place. Save `.agnostic-ai/mcps/docs-search.yaml` to define an HTTP server without enabling it: ```yaml name: docs-search type: http url: https://mcp.example.com/mcp disabled: true ``` After sync, `.pi/mcp.json` contains the server with `enabled: false`. Replace the example URL with your server's endpoint before enabling it. ## Limits and trust - Project settings, MCP, skills, and prompt templates need Pi's project trust approval. Instruction files load without that approval. Trust remains Pi's decision; sync does not write trust approvals or `defaultProjectTrust`. - Pi has no native agent definitions, lifecycle hooks, or per-tool permission rules. Agent, hook, ignore, review, and environment specs have no native output; portable permission settings are unsupported. Skill `allowed-tools` is metadata, not an enforced permission limit. - MCP supports stdio and HTTP; SSE is unsupported. Invalid global MCP specifications stop sync before native files change. - The default prompt directory loads direct Markdown children. Nested command names are unsupported. - Commands sync only at project scope. Global command sync is outside this target's support. ## Global configuration `agnostic-ai sync --global -t pi` writes instructions, skills, MCP, and settings under `~/.pi/agent/`. `PI_CODING_AGENT_DIR` replaces that whole agent directory. See [global configuration](https://agnostic-ai.org/docs/global-configuration/). ## Config keys For the shared root instruction file, see [entry-point files](https://agnostic-ai.org/docs/configuration/#entry-point-files). | Key | Default | |---|---| | `outputs.pi.rules-file` | unset, uses instruction files | | `outputs.pi.skills-dir` | `.pi/skills` | | `outputs.pi.commands-dir` | `.pi/prompts` | | `outputs.pi.mcp-file` | `.pi/mcp.json` | | `outputs.pi.conf-file` | `.pi/settings.json` | Changing a path does not change where Pi looks for it. Keep overrides aligned with your native configuration. ## Import ```bash agnostic-ai import pi agnostic-ai lint agnostic-ai sync -t pi agnostic-ai sync --check -t pi ``` Import reads root and scoped instructions, skill folders and resources, direct prompt templates, `.pi/mcp.json`, and `.pi/settings.json`. Native MCP `enabled` is inverted back to portable `disabled`. Project control-only overrides import with `targets: [pi]`. Model and thinking level become portable settings; `defaultProvider` stays under `x-pi`. Use `agnostic-ai import --global pi` for supported user-level files. ## Protected paths Not enforced. A settings spec with a `protected` block produces a coverage note. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Cursor (`cursor`) Cursor reads its project configuration from `.cursor/`, including `.cursor/rules/`. ## First rule Install the client using the [official setup guide](https://cursor.com), then open your project in it. Select `cursor` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.cursor/rules/conventional-commits.mdc`. The IDE and CLI share rules, but the permission settings below guard only the Cursor CLI. For existing files, check [what `import cursor` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.cursor/rules/conventional-commits.mdc` for the commit-prefix rule. Open the project in Cursor. Confirm the rule is Always Apply in the Rules panel, then ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` .cursor/rules/.mdc .cursor/agents/.md # one native subagent per agent spec .cursor/skills//SKILL.md # one folder per skill, bundled assets included .cursor/commands/.md # one per command spec .cursor/hooks.json # when hook specs exist (sync updates its entries, keeps yours) .cursor/hooks/agnostic-ai-portable-hook.sh # when a portable before-tool hook exists .cursor/mcp.json # when MCP entries exist .cursor/cli.json # from settings permissions and deny protected paths (merged) ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Rules `alwaysApply: true`, or `false` when the spec sets `globs` without `alwaysApply`. - A catch-all such as `**/*` stays always-on (override in frontmatter). An always-apply rule omits `globs`. - A non-always rule without `globs` falls back to the `paths` list (comma-joined). - With neither, `globs` is omitted, so Cursor treats the rule as "Apply Intelligently" (description-driven) or "Apply Manually". ### Per-file check `agnostic-ai explain --file --target cursor` checks each planned `.mdc` rule and every `AGENTS.md` Cursor reads against one file, using the [Rules](https://cursor.com/docs/rules) matrix. ### Agents [Cursor subagents](https://cursor.com/docs/subagents.md) (Cursor 2.4+). Frontmatter has `name`, `description`, and any declared `model`, `readonly`, and `is_background`. The portable `background` is written as `is_background`, and an `is_background` the spec sets wins. The body is the system prompt. - Cursor has no `tools` field, so sync drops a `tools` list with a sync note. Use `readonly: true` instead. - Cursor has no effort field, so a portable `effort` raises a sync note. Put it in the model id: `model: {cursor: "claude-opus-5[effort=high]"}`. See [per-target `model` and `effort`](https://agnostic-ai.org/docs/spec-format/agents/#per-target-model-and-effort). ### Commands Markdown files under `.cursor/commands/`, body as the prompt. Cursor no longer documents this path; see its [migrate commands to skills](https://cursor.com/help/customization/skills.md) FAQ. ### Old copies Sync removes older flattened `.mdc`, agent-as-command, and `skill-.mdc` files. ### Skills `.cursor/skills//SKILL.md`, the [Agent Skills](https://cursor.com/docs/skills.md) layout (Cursor 2.4+), bundled files copied unchanged. - Frontmatter has `name`, `description`, and any declared `paths`, `disable-model-invocation`, `icon`, `color`, and `metadata`. `icon` and `color` style the badge when the skill backs a [Custom Mode](https://cursor.com/docs/agent/prompting.md#custom-modes). - A directory scope from the source folder moves the folder under that directory. - Cursor loads skills only from the workspace it opens, so a skill's `workspaces` list adds a `/.cursor/skills//` copy per directory, beside the root copy. - Skill `model` and `effort` are omitted with a sync note when a value resolves for Cursor. ### Review [Bugbot](https://cursor.com/docs/bugbot) files, root `.cursor/BUGBOT.md` for specs with no directory scope and `/.cursor/BUGBOT.md` for scoped ones. - Same-scope specs are joined into one file. - [Rule limits](https://cursor.com/docs/bugbot#rule-limits): Bugbot truncates each BUGBOT.md at 30,000 characters and caps one review's rules at 100,000 in total. - agnostic-ai never truncates. An over-cap file is written in full, with a sync note. The size counts the whole file, header included, so several small specs joined can pass the cap. Split the review across sibling scopes. - `sync` also notes a scope whose chain (the scope and its parent directories) passes 100,000 characters. Team and repository rules share that budget, so a shorter chain can still lose rules. To see what Bugbot dropped, run `bugbot run verbose=true` or `cursor review verbose=true` on a pull request. ### Environment `.cursor/environment.json` ([background-agent](https://docs.cursor.com/background-agent) setup). - Spec keys pass through as written, except fields that select a tool or directory and `cleanup`, which gets a no-effect note. Specs merge by top-level key. - `setup` and `setup-windows` go to `.cursor/worktrees.json` as `setup-worktree` (every OS) and `setup-worktree-windows` (Windows only) ([worktrees](https://cursor.com/docs/configuration/worktrees)). - A script path, which Cursor resolves from `.cursor/`, or a unix-only `setup-worktree-unix`, goes under `x-cursor:` with its native key, such as `setup-worktree-unix: setup.sh`. ### Ignore `.cursorignore` (gitignore syntax). Specs are joined into one file. ### Hooks [Cursor Hooks](https://cursor.com/docs/hooks) in a managed `.cursor/hooks.json` (`version` + per-event arrays). With `builtins: [memory]`, a `sessionStart` hook adds the [shared memory](https://agnostic-ai.org/docs/memory/) index to the session. - A `type: prompt` hook writes `prompt` and optional `model`. Command and prompt hooks keep `timeout`, `loop_limit` (including `null`), `failClosed`, and `matcher`. - camelCase event names (`beforeShellExecution`, `afterFileEdit`, ...) pass through. `validate` flags unknown ones. - Fifteen events take a `matcher`: `preToolUse`, `postToolUse`, `postToolUseFailure` (tool name), `subagentStart`, `subagentStop` (subagent type), `beforeShellExecution`, `afterShellExecution` (full command string), `beforeReadFile`, `afterFileEdit` (tool name), and `beforeTabFileRead`, `afterTabFileEdit`, `beforeSubmitPrompt`, `stop`, `afterAgentResponse`, `afterAgentThought` (one fixed value each). `lint` flags a matcher on any other event, except `beforeMCPExecution` and `afterMCPExecution`. - When a command hook exists, sync adds a `sessionStart` hook that prints `{"env":{"AGNOSTIC_AI_TARGET":"cursor"}}`. Cursor passes that `env` to every later hook in the session, including loaded Claude Code hooks, and on Windows too. `sessionStart` hooks, and any hook that fires before this one returns, do not see the variable. See [which target ran a hook](https://agnostic-ai.org/docs/spec-format/hook-reference/#hook-target). - A hook has only `command`, run through `$SHELL -c` (PowerShell on Windows). A spec's `args` fold into `command`, each in single quotes (`node 'guard.js'`). - A [portable](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) `before-tool` hook lands on `preToolUse` and a `prompt-submit` hook on `beforeSubmitPrompt`, each through `.cursor/hooks/agnostic-ai-portable-hook.sh`. Exit 2 becomes a deny reply with stderr as the message, and exit 0 an allow reply. `session-start` and `session-end` land on `sessionStart` and `sessionEnd`. `sync --global` writes the wrapper to `~/.cursor/hooks/`. ### MCP `.cursor/mcp.json` under `mcpServers`. Each sync replaces the whole file from MCP specs. Cursor-only fields ([cursor.com/docs/mcp](https://cursor.com/docs/mcp.md)): - A stdio server accepts `envFile`, a path to an env file with extra variables. - A `${NAME}` reference in `env`, `headers`, `url`, or `args` is written as Cursor's `${env:NAME}`; `${workspaceFolder}` and Cursor's other variables stay as written. See [environment references](https://agnostic-ai.org/docs/spec-format/mcps/#environment-references). - A remote (`url`) server accepts a static-OAuth `auth`, `{CLIENT_ID, CLIENT_SECRET, scopes}` with `CLIENT_ID` required, for providers without OAuth Dynamic Client Registration. - A stdio server carries an explicit `"type": "stdio"`, which Cursor requires. - A `roots` list passes through, though Cursor documents no per-server `roots` key. - `disabled: true` has no effect here; see [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target). ### Hook args with apostrophes or spaces PowerShell reads that quoting the same way unless an arg has an apostrophe. A command path with a space is quoted too, and PowerShell does not run it. Use a shell-form `command` in those cases. ### Third-party reads The **Include Third-Party Plugins, Skills, and Other Configs** setting (Cursor Settings → Agents → Third-Party Imports, [on by default](https://cursor.com/docs/reference/third-party-hooks.md)) turns on Claude and Codex compatibility reads. Turn it off to stop those reads, or give the spec a single `target:`. Cursor reads `.agents/skills/` (written by `codex`) either way. - **Skills** ([docs](https://cursor.com/docs/skills.md)): Native project roots are `.cursor/skills/` and `.agents/skills/`. Compatibility roots include `.claude/skills/`, `.codex/skills/`, and their home equivalents. Cursor does not document which root wins on a duplicate. - **Subagents** ([docs](https://cursor.com/docs/subagents.md)): Cursor reads `.claude/agents/` and `.codex/agents/` (current project only) beside `.cursor/agents/`. On a name clash, `.cursor/` wins. Cursor documents only Markdown agents, so it may not parse the `codex` target's TOML files. - **Hooks** ([docs](https://cursor.com/docs/reference/third-party-hooks.md)): Cursor merges seven hook sources, including `.cursor/hooks.json` and `.claude/settings.json`, runs every matching hook, and maps Claude event names to Cursor ones. A repo syncing `claude` and `cursor` runs every hook twice, except a portable hook, whose Claude Code copy exits under Cursor ([details](https://agnostic-ai.org/docs/spec-format/hook-reference/#claude-settings-copies)). The setting is the only way to stop the rest. - **Hook `args`**: Cursor's third-party hooks docs do not list `args`, so a claude hook spec with `args` may run as a bare interpreter that reads the JSON event data as its program. `sync` notes this when `claude` runs and `cursor` is configured. Use a shell-form `command`, or turn off the setting. ## Config keys | Key | Default | |---|---| | `outputs.cursor.rules-dir` | `.cursor/rules` | | `outputs.cursor.agents-dir` | `.cursor/agents` | | `outputs.cursor.skills-dir` | `.cursor/skills` | | `outputs.cursor.commands-dir` | `.cursor/commands` | | `outputs.cursor.mcp-file` | `.cursor/mcp.json` | | `outputs.cursor.review-file` | `BUGBOT.md` | | `outputs.cursor.environment-file` | `.cursor/environment.json` | | `outputs.cursor.ignore-file` | `.cursorignore` | | `outputs.cursor.hooks-file` | `.cursor/hooks.json` | ## Permissions A settings spec's portable `allow` and `deny` lists become Cursor CLI rules in `permissions.allow` and `permissions.deny` of `.cursor/cli.json`, beside the protected-path rules. Sync writes only spellings the [CLI permissions](https://cursor.com/docs/cli/reference/permissions) page documents, and never widens a rule. The rules guard the Cursor CLI only, not the IDE agent. | Portable rule | Cursor rule | Notes | |---|---|---| | `Bash(git:*)`, `Bash(git *)` | `Shell(git)` | One command word only. `Shell(git)` covers bare `git` too. | | `Read()` | `Read()` | `/path` loses its `/`, since Cursor scopes a relative path to the workspace. `//path` becomes the absolute `/path`. On `deny`, a path with no `*` or `?` also gets `Read(/**)`. | | `Edit()`, `Write()` | `Write()` | Same path rules as `Read`. | | `WebFetch(domain:)` | `WebFetch()` | An exact host, `*.host`, or `*`. | | `mcp____` | `Mcp(:)` | `mcp__` becomes `Mcp(:*)`. On `deny` only, `mcp__*` becomes `Mcp(*:*)` and `mcp__*__` becomes `Mcp(*:)`. | These stay out: - A multi-word command such as `Bash(go test:*)` or `Bash(git push:*)`. `Shell` takes one command word, and `Shell(go)` would cover every `go` command. - An exact command such as `Bash(ls)` or `Bash(rm)`, since `Shell(rm)` also matches `rm` with arguments. - A path starting with `~` or `!`, or using `[]`, `{}`, or `\`. - A `WebFetch` host with a path, a port, or a wildcard other than a leading `*.`. - `ask` rules. The CLI has no ask list and already prompts before a call no `allow` rule covers. - Other tools, such as `WebSearch`, `Grep`, or a bare `Bash`. A dropped `allow` rule raises a sync note. A dropped `deny` rule loosens the policy, so sync names each one: `deny rule not enforced on cursor: Bash(git push:*)`. Block those with a [Cursor hook](https://cursor.com/docs/hooks) on `beforeShellExecution`. Cursor does not say how `Shell` matches a chained command such as `git status && rm -rf build`, or a pipe. Claude Code checks each subcommand of a chain, so an `allow` rule may approve more in the Cursor CLI. A relative path means the current directory in Claude Code and the workspace in Cursor. A rule you remove from a spec leaves `cli.json` on the next sync. A rule you wrote there yourself stays. Sync always writes both lists, since the Cursor CLI rejects a `permissions` object without one, and removes the file it created once no rule is left. With `memory.personal: repo`, sync also allows writes to the [personal memory folder](https://agnostic-ai.org/docs/memory/#one-store-per-repository). A folder path with pattern characters, including parentheses, gets no rule and a note; move `AGNOSTIC_AI_HOME` or set `memory.personal: checkout`. ## Import `agnostic-ai import cursor` reads: | Source | Becomes | |--------|---------| | `.cursor/rules/.mdc` | `/.md`, frontmatter kept as written | | `.cursor/rules//.mdc` | `//.md` | | (no `name:` in frontmatter) | `name:` added from the filename | | `.cursor/agents/.md` | `/.md`, generated header stripped | | `.cursor/skills//` and `.agents/skills//` | `//`, full folder and all its files, SKILL.md merged onto the existing spec | | `.cursor/commands/.md` | `/.md`, generated header stripped | | `.cursor/BUGBOT.md` | `/review.md` | | `/.cursor/BUGBOT.md` | `/.md` with `scope: `, such as `services-api.md` for `services/api` | Import skips a `BUGBOT.md` that sync wrote. Both skill directories are project-level, at the root and in nested subdirectories. The nesting becomes the spec scope. `.cursor/skills` wins a same-name clash at the same scope. - Cursor writes no `argument-hint` or `allowed-tools` on a skill, so import leaves both on the spec. - Keys Cursor does write, such as a skill's `icon` or an agent's `model`, follow the native file. - A rule's frontmatter comes from the `.mdc` alone, so widening `globs` to `**/*` unscopes the spec. - A rule with neither `alwaysApply` nor `globs` gets `alwaysApply: false`, since Cursor does not load it on every request. A rule scoped to a directory gets no flag; it already loads only there. `import cursor` also reads environment files: - A hand-written `worktrees.json` becomes `environments/worktree.yaml`, mapped the same way sync writes it. - A hand-written `environment.json` (comments allowed) becomes `environments/cursor.yaml`, keeping every key and turning `//` comments into YAML comments. A file that sets a key the spec reads itself, such as `name` or `setup`, stays as written with a note. ## Protected paths Enforced (permission). In the Cursor CLI, each path of a `decision: deny` block becomes `Write()` and `Write(/**)` rules in `permissions.deny` of `.cursor/cli.json`, the project CLI config ([configuration](https://cursor.com/docs/cli/reference/configuration)). A path that already ends in `**` gets the first rule only. The rules carry no leading `/`: Cursor scopes a relative path to the workspace and reads a leading `/` as an absolute path ([CLI permissions](https://cursor.com/docs/cli/reference/permissions)). - **IDE agent**: not covered ([permissions](https://cursor.com/docs/reference/permissions.md)). State the paths in a rule if the IDE agent should know about them. - **`decision: ask`**: not enforced, with a sync note. The CLI has no ask list and already prompts before a write no `allow` rule covers. Use `decision: deny` to block. - **`reason`**: not written. A CLI permission rule has no message field. - **Other settings fields**: `model`, `effort`, and `x-cursor` raise a sync note, since a project `cli.json` takes `permissions` only. `cli.json` merges as [Permissions](#permissions) describes: your own rules and other keys stay, and removing a path or its block removes its rules on the next sync. A matching rule that was in `cli.json` before sync added one stays yours. See [Protected paths](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # GitHub Copilot (`copilot`) GitHub Copilot reads most project configuration from `.github/`, and VS Code reads MCP servers from `.vscode/mcp.json`. ## First rule Install the client using the [official setup guide](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot), then open your project in it. Select `copilot` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.github/instructions/conventional-commits.instructions.md`. VS Code, Copilot CLI, and the cloud agent read different settings and MCP files. The reference below names the surface for each output. For existing files, check [what `import copilot` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.github/instructions/conventional-commits.instructions.md` for the commit-prefix rule. Open the project in VS Code with Copilot. Ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` .github/copilot-instructions.md # entry-point file with the AGNOSTIC_AI.md body .github/instructions/.instructions.md # scoped rule per file .github/agents/.agent.md # one custom-agent profile per agent .github/skills//SKILL.md # one folder per skill, bundled assets included .vscode/mcp.json # when MCP entries exist; VS Code's file (deprecated in 1.140), servers key .github/mcp.json # when MCP entries exist; Copilot CLI's file, mcpServers key .mcp.json # only with outputs.copilot.root-mcp-file; mcpServers key .github/hooks/agnostic-ai.json # when hook entries exist .github/hooks/scripts/agnostic-ai-portable-hook.sh # when a portable before-tool hook exists .github/copilot/settings.json # when a Settings model or a disabled MCP server exists .vscode/settings.json # chat.useAgentsMdFile: false, when another target writes AGENTS.md ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Instructions `.github/copilot-instructions.md` holds the whole `AGNOSTIC_AI.md` body, even when another target writes a root `AGENTS.md`. Copilot Chat on GitHub.com, Visual Studio, and JetBrains, and code review in the IDEs, read that file and not `AGENTS.md` ([support by surface](https://docs.github.com/en/copilot/reference/custom-instructions-support)). - VS Code chat, the cloud agent, and GitHub.com code review can also read `AGENTS.md`, so the shared body can load twice. Sync sets `chat.useAgentsMdFile: false` in `.vscode/settings.json` for VS Code's local agent in a trusted workspace. This also disables nested `AGENTS.md`; native `.instructions.md` files carry those rules instead. - Your explicit setting wins. A true value gets a double-load note. Removing Copilot or the last `AGENTS.md` writer removes the generated setting. Cloud and code-review surfaces have no such switch. See [support by surface](https://docs.github.com/en/copilot/reference/custom-instructions-support). ### Rules A rule with `globs` or a folder scope (like `rules/backend/auth.md`) gets its own `.instructions.md` file with `applyTo`. - Scope and patterns combine: `scope: src/a` with `globs: tests/a/**` writes `applyTo: src/a/**,tests/a/**`. - An always-on rule with no scope (no globs, or `alwaysApply: true`) gets `applyTo: "**"`. - A rule with `alwaysApply: false` and no scope or globs gets no `applyTo`. VS Code loads it when its `description` matches the task ([custom instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions)). `import copilot` reads it back the same way. - `description` and every `x-copilot` key go next to `applyTo`. ### Agents [custom agent profiles](https://docs.github.com/en/copilot/reference/custom-agents-configuration) at `.github/agents/.agent.md`. An existing `.md` keeps its path. VS Code reads any `.md` there, so a second file would load twice. - Frontmatter has `name`, `description`, and `tools` and `model` when set. - `x-copilot.name` sets a display name that differs from the file name. Import fills it when the two differ. - Other `x-copilot` keys (`target`, `user-invocable`, `mcp-servers`, `effort`, ...) are copied as written. - The profile has no effort key, so a portable `effort` gets a sync note. Per-agent `effortLevel` exists only in the `subagents.agents` setting of your personal `~/.copilot/settings.json`, which `sync --global` writes (see [global output](https://agnostic-ai.org/docs/global-configuration/#global-output)). ### Skills [Copilot skills](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills) at `.github/skills//SKILL.md`. Copilot also reads `.claude/skills/` and `.agents/skills/`. A skill found there and not in `.github/skills/` is written in place. `outputs.copilot.skills-dir` sends every skill to one directory. `SKILL.md` keeps `argument-hint` and `disable-model-invocation`, unless another target writes the same skills folder ([agent skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills)). Copilot has no commands, so each command becomes a skill there that only the user starts from the `/` menu ([commands](https://agnostic-ai.org/docs/spec-format/commands/#tools-without-commands)). ### Old copies Sync removes old `agent-.instructions.md` and `skill-.instructions.md` files. ### Chat modes With `outputs.copilot.chatmodes-dir` set, each agent is also written as a VS Code [Custom Chat Mode](https://code.visualstudio.com/docs/copilot/customization/custom-chat-modes) at `/.chatmode.md` with `description`, `model`, and `tools` frontmatter. ### Settings `.github/copilot/settings.json` keeps its other keys and gets: - the last portable `model`, as the Copilot CLI repository default. - a portable `effort` of `low`, `medium`, `high`, or `xhigh`, as `effortLevel`. Other values get a sync note. - a settings spec's `x-copilot` block, for keys agnostic-ai has no field for, such as `respectGitignore`. - `x-copilot.enabledPlugins` and `x-copilot.extraKnownMarketplaces`, the repository's default plugins and plugin marketplaces. Copilot CLI and Copilot in the cloud both read them. Repository values override personal ones for `enabledPlugins`; a marketplace's `autoUpdate` has no effect at this level ([CLI config reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference)). - `x-copilot.deniedUrls`, Copilot's blocked URL list. No portable rule sets it. `allowedUrls` works only in your personal settings. - every MCP spec with `disabled: true`, sorted, in `disabledMcpServers`. It is the only file setting that stops a configured server from starting ([CLI config reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference)). - `import copilot` restores the model to `settings/copilot.yaml` and `effortLevel` as `effort` (or `x-copilot.effortLevel` when another settings spec sets a different effort), and keeps `enabledPlugins` and `extraKnownMarketplaces` unchanged under `x-copilot`. Other keys only Copilot uses stay in the native file. ### MCP VS Code and Copilot CLI read different files, so sync writes both. Override them with `outputs.copilot.mcp-file` and `outputs.copilot.cli-mcp-file`. - `.vscode/mcp.json` uses the VS Code schema: top-level `servers`, each with a `type` (`stdio`, `http`, or `sse`). VS Code passes it to Copilot harness sessions, except servers that need interactive input ([VS Code MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers)). Sync owns `servers` and keeps other top-level keys, such as `inputs` and `sandbox`. It strips JSONC comments and reformats the file. Invalid JSONC stops the write. - `.github/mcp.json` carries the same servers under `mcpServers`, since Copilot CLI rejects the `servers` key ([Copilot CLI MCP docs](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers)). Sync owns the whole file, and the whole root copy. - A per-server `tools` list of allowed tools (default `*`) goes only to `.github/mcp.json` and the root copy. VS Code documents no such key, and Codex's `tools` means something else. - A `roots` list is copied as written. Neither VS Code nor GitHub documents it per server. - Only `.vscode/mcp.json` gets the VS Code fields ([VS Code MCP configuration](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)). Set them at the spec's top level or under `x-copilot`; `x-copilot` wins: - stdio servers: `cwd`, `envFile` (for example `${workspaceFolder}/.env`), and `sandboxEnabled` (macOS and Linux only). - stdio servers: `sandbox` with `filesystem.allowWrite`, `filesystem.denyRead`, `filesystem.denyWrite`, `network.allowedDomains`, and `network.deniedDomains`, each a list. VS Code reads allow rules only here, not in the top-level `sandbox`. Other keys are dropped. - remote servers: `oauth: {clientId, enterpriseManaged}`. - all servers: `dev.watch`, a glob or list of globs that restarts the server on change. - stdio servers: `dev.debug` (`debug: {type: "node"|"debugpy", debugpyPath}`). On a remote server it gets a sync note, and its watch patterns are still written. - VS Code 1.140 calls `.vscode/mcp.json` deprecated but still loads it, with no removal date. **MCP: Add Server** offers to save workspace servers to a root `.mcp.json` under the `mcpServers` key, only when `chat.mcp.workspaceRootConfig.enabled` is on (experimental) ([VS Code MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers), [VS Code MCP configuration](https://code.visualstudio.com/docs/agents/reference/mcp-configuration), [1.140 release notes](https://code.visualstudio.com/updates/v1_140)). A project without the `claude` tool can opt in: ```yaml outputs: copilot: root-mcp-file: .mcp.json ``` Sync then also writes the servers there, with the `tools` list of allowed tools and without the VS Code fields. ### Why root .mcp.json is opt-in VS Code always reads a root `.mcp.json`. In a trusted workspace it starts those servers, along with the ones from `.vscode/mcp.json`, with no prompt. VS Code does not say which file wins when both define the same server name. The `claude` target writes `.mcp.json` too. A plain server is identical from both, so sync writes it once. A field only one tool documents, such as Claude Code's `timeout` or Copilot CLI's `tools`, makes the copies differ, and sync stops with an conflicting output files. VS Code does not say which copy wins ([design notes on #1533](https://github.com/Chemaclass/agnostic-ai/issues/1533#issuecomment-5919420911)). ### Hooks `sync --global` writes user hooks to `~/.copilot/hooks/agnostic-ai.json`, or `$COPILOT_HOME/hooks/agnostic-ai.json` when set. Copilot CLI combines all user `*.json` hook files with repository hooks. This native user route was checked against the [hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference) on October 11, 2026. `import --global copilot` reads those user files. It keeps matching handwritten handlers in their original file, so sync adds no duplicate. Unsupported entries stay native with a skip reason. If an imported neighboring handler changes, sync stops and names its file. Make the native entry match the spec, or move the original file outside the hooks directory before syncing. On a fresh machine, sync creates the imported handlers in the managed file. Hooks go to `.github/hooks/agnostic-ai.json` (override with `outputs.copilot.hooks-file`). Copilot CLI and cloud agent load every `.github/hooks/*.json` and merge them ([hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference)). Copilot documents 14 events. With `builtins: [memory]`, a `SessionStart` hook adds the [shared memory](https://agnostic-ai.org/docs/memory/) index to the session. Its reply holds the index under `additionalContext`, which Copilot CLI reads, and under `hookSpecificOutput.additionalContext` with `hookEventName: "SessionStart"`, which VS Code reads. A spec with `commandWindows` writes `bash` (from `command`) and `powershell` (from `commandWindows`) instead of one `command`, which Copilot would run in both shells. - The wrapper is `{"version": 1, "hooks": {...}}` with an **integer** version. Each entry is flat, `{"type": "command", "matcher": ..., "command": ..., "timeoutSec": ...}`, not Claude Code's nested `{matcher, hooks: [...]}` group. - `event:` is copied as written. Copilot accepts PascalCase (`PreToolUse`, the "VS Code compatible" format) and camelCase (`preToolUse`). - PascalCase matches the way Claude Code does and uses its tool names, so `matcher: Bash` works unchanged. - camelCase matches the whole value against the matcher as a regex. The value depends on the event: Copilot's lowercase tool names (`bash`, `edit`, `view`, ...) on `preToolUse`, `postToolUse`, and `permissionRequest`, the subagent's name on `subagentStart`, `notification_type` on `notification`, and `manual` or `auto` on `preCompact` ([matcher filtering](https://docs.github.com/en/copilot/reference/hooks-reference#matcher-filtering)). - Copilot skips a hook with an invalid regex matcher. `sync` reports that, a Claude tool name on a camelCase event, and a matcher on an event that documents none. - `userPromptTransformed` and `subagentStart` exist only in camelCase. A PascalCase `SubagentStart` (valid in Claude Code and Codex) never fires, and `validate` flags it. - Command hooks also take `cwd` (relative to the repository root, or absolute) and `env` (with variable expansion), set at the spec's top level or under `x-copilot`. Both are Copilot-only and survive import. - With a relative `cwd`, sync writes the script path relative to that directory. So `.agnostic-ai/hooks/guard.sh` with `cwd: sub` becomes `../.agnostic-ai/hooks/guard.sh`, and import restores the repository-relative path. - The path is the first word of `command`, or the second after an interpreter such as `bash`, or it is in `exec` and `args`. - Absolute paths, `$`-prefixed paths, and commands that are not paths stay as written. So does every path when `cwd` is absolute or outside the repository. - `import copilot` reads back command handlers, HTTP handlers (`url`, `headers`, `allowedEnvVars`), and `sessionStart` prompt handlers. - A spec with `args` is written in Copilot's form without a shell, `{"type": "command", "exec": , "args": [...]}`, for paths or arguments with spaces. Copilot does not allow `exec` next to `command`, so the program moves out of `command`. Claude Code's form keeps it there. Only Copilot CLI runs this form. Cloud agent runs only `bash` or `command` entries, so `sync` adds a sync note. Leave `args` unset for a hook that must run under cloud agent. - A [portable](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) `before-tool` hook lands on `PreToolUse` and runs through `.github/hooks/scripts/agnostic-ai-portable-hook.sh`. Exit 2 denies the call and uses stderr as `permissionDecisionReason`. Exit 1 also denies, so a broken guard keeps blocking. Claude Code reports an error and goes on instead. A hook with [`decision: stdout`](https://agnostic-ai.org/docs/spec-format/hook-reference/#decision-on-stdout) runs through the same wrapper. Import reads it back as the portable spec. **Every hook runs twice when you sync `claude` and `copilot` together.** Copilot also reads `.claude/settings.json` and `.claude/settings.local.json` and runs every entry for an event. Both are default targets, so a formatter runs twice, an audit hook writes twice, and a blocking `preToolUse` returns two decisions. Unlike Cursor and Trae, Copilot has no switch to stop this. Give the hook spec a single `target:`. **Copilot reads Claude settings too.** Copilot CLI reads `companyAnnouncements`, `disableAllHooks`, `enabledPlugins`, `extraKnownMarketplaces`, and `hooks` from `.claude/settings.json` and `.claude/settings.local.json` ([CLI config reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference)). agnostic-ai writes `hooks`, and `enabledPlugins` via `outputs.claude.settings.enabledPlugins`, so that key also sets the Copilot CLI plugin policy for the repository. Set it on purpose, or drop `claude` if the two tools need different plugins. GitHub documents only the `.github/copilot/settings.json` keys as read by Copilot in the cloud, so for plugins both should load, use `x-copilot.enabledPlugins` (see [Settings](#settings)). ## Config keys | Key | Default | Notes | |---|---|---| | `outputs.copilot.instructions-dir` | `.github/instructions` | | | `outputs.copilot.agents-dir` | `.github/agents` | | | `outputs.copilot.skills-dir` | `.github/skills` | | | `outputs.copilot.mcp-file` | `.vscode/mcp.json` | | | `outputs.copilot.cli-mcp-file` | `.github/mcp.json` | Copilot CLI's own file, `mcpServers` key | | `outputs.copilot.root-mcp-file` | unset, opt-in | writes the same servers to `.mcp.json` in the workspace root, under the `mcpServers` key | | `outputs.copilot.chatmodes-dir` | empty, opt-in | writes one Custom Chat Mode per agent | | `outputs.copilot.rules-file` | unset | writes all always-on rules into that one file and skips the instructions that point to source specs | | `outputs.copilot.hooks-file` | `.github/hooks/agnostic-ai.json` | | ## Import `agnostic-ai import copilot` reads: | Source | Becomes | |---|---| | `.github/copilot-instructions.md` | `.agnostic-ai/AGNOSTIC_AI.md`, whole. Only a rules block that `sync` wrote also becomes `/.md` specs | | `.github/instructions/.instructions.md` | `/.md`. Files with an `agent-` or `skill-` prefix become agents and skills | | `.github/agents/.agent.md` and `.github/chatmodes/.chatmode.md` | `/.md`, keeping the frontmatter keys Copilot output does not use | | `.github/skills//`, `.claude/skills//`, `.agents/skills//` | `//` | | `.github/hooks/*.json` | one target-scoped hook spec per handler | | `.vscode/mcp.json` | `/.yaml` | | `.github/copilot/settings.json` `model`, `effortLevel`, `enabledPlugins`, `extraKnownMarketplaces` | `/copilot.yaml`: `model`, `effort` as in [Settings](#settings), and the plugin keys under `x-copilot`, unless another settings spec for Copilot sets them | All three skill directories are [documented](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills). On a name clash, `.github/skills` wins, then `.claude/skills`, then `.agents/skills`. ## Protected paths Not enforced. Copilot takes deny and ask rules only from device settings managed by your organization, not from a repository file, so sync prints a note for [protected paths](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). State the paths in a rule if the agent should know about them. # Aider (`aider`) [Aider](https://aider.chat/docs/config/aider_conf.html) reads rules from `CONVENTIONS.md`. agnostic-ai writes that file, an optional `.aider.conf.yml` that loads it, and `.aiderignore`. ## First rule Install the client using the [official setup guide](https://aider.chat/docs/install.html), then open your project in it. Select `aider` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `CONVENTIONS.md`. Aider does not load this file automatically unless you enable the [project config](#output). For existing files, review [migration](https://agnostic-ai.org/docs/migration/) and the [`import aider` command](https://agnostic-ai.org/docs/cli-reference/start/#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `CONVENTIONS.md` for the commit-prefix rule. Start Aider with `aider --read CONVENTIONS.md`. Ask it to write a commit subject for a small fix; it should use `fix:`. Next, [enable automatic rule loading](#output) so you do not need `--read` on each launch. ## Output ``` CONVENTIONS.md # pointer body + inlined rules block (written by sync) .aider.conf.yml # only when conf-file is set .aiderignore # only when the project has ignore entries ``` - **Rules**: `CONVENTIONS.md` holds the instructions that point to source specs plus a marked `## Rules` block with the project-wide rule bodies. `import aider` strips that block. Load it with `aider --read CONVENTIONS.md`. - **Auto-load**: set `outputs.aider.conf-file: .aider.conf.yml` to add a `read:` entry to Aider's [project config](https://aider.chat/docs/config/aider_conf.html). `model` and `weak-model` go into the same file when set. Your other keys stay, and the `read:` list has no duplicates. Each sync rewrites the file with sorted keys and drops its comments. - **Ignore**: ignore entries go to `.aiderignore` in the project root. That is Aider's default path ([Aider config docs](https://aider.chat/docs/config/aider_conf.html)), so it needs no setup. If you point `outputs.aider.ignore-file` elsewhere, Aider reads it only when the config file's `aiderignore:` key names that path. ### What sync removes from .aider.conf.yml When aider leaves `targets` or `conf-file` is unset, sync removes only `model`, `weak-model`, and the `read:` entry it added. It preserves your key order and remaining comments, and removes the header. When `rules-file` changes, sync removes the `read:` entry it added for the old path. ## Config keys | Key | Default | Notes | |---|---|---| | `outputs.aider.conf-file` | empty, opt-in | | | `outputs.aider.model` | | | | `outputs.aider.weak-model` | | | | `outputs.aider.rules-file` | unset | writes all rules into one legacy file and skips the write of instructions that point to source specs | | `outputs.aider.ignore-file` | `.aiderignore` | | With [`builtins: [handoff]`](https://agnostic-ai.org/docs/handoff/), a configured `rules-file` includes the handoff instructions. ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Cline (`cline`) Cline gets `AGENTS.md`, rules, agents, skills, and hook scripts. Workflows are opt-in. ## First rule Install the client using the [official setup guide](https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev), then open your project in it. Select `cline` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.clinerules/conventional-commits.md`, or shared `AGENTS.md` when another selected target already puts the rule there. When another target includes this rule in `AGENTS.md`, Cline uses that shared copy instead. Keep `AGENTS.md` enabled in the Rules panel. For existing files, check [what `import cline` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and find the commit-prefix rule in `.clinerules/conventional-commits.md` or the shared `AGENTS.md`. Open the project in Cline and confirm the rule is enabled in its Rules panel. Ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # pointer body, plus the rules block when another tool adds it .clinerules/.md # one per rule that AGENTS.md does not carry, or that needs `paths` .cline/agents/.yml # frontmatter over a Markdown system prompt .clinerules/hooks/ # one executable script per hook event, no extension .cline/skills//SKILL.md # one folder per skill (Cline's recommended skills path) .clinerules/workflows/.md # one per agent, only when workflows-dir is set ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: one file each under `.clinerules/`. - When Codex or another tool that copies rules into `AGENTS.md` writes that file, its `## Rules` block holds every rule without a scope. Cline loads that file by default, so an always-on rule gets no `.clinerules/` file and loads once. - Rules with `paths` keep their file. So does a rule whose text differs for Cline (a `::target cline` fence or a path variable). - Keep `AGENTS.md` on in Cline's Rules panel. Otherwise Cline misses the rules that have no file of their own. - Listing `AGENTS.md` under `sync.unmanaged` makes every rule keep its file. Check [the effect on other targets sharing that file](https://agnostic-ai.org/docs/target-behavior/#inlined-rules). - **Rules layouts**: Cline [searches both](https://docs.cline.bot/customization/cline-rules) `.clinerules/` and `.cline/rules/` in VS Code, Desktop, and the CLI, and [deprecates](https://docs.cline.bot/resources/deprecations) neither. `.clinerules/` is the default because the VS Code Rules panel creates rules there (`outputs.cline.rules-dir: .cline/rules` picks the other). Sync writes one and removes the files it wrote earlier in the other, so rules never load twice. - **Rule activation**: a scoped rule gets `paths` limited to its folder, even with `alwaysApply: true`. A rule without a scope gets native `paths` from its file selector with `alwaysApply: false`, or with `globs` other than a catch-all such as `**/*` and no `alwaysApply`. With no usable selector, `sync` reports it. See [Cline conditions](https://docs.cline.bot/customization/cline-rules) and [file-pattern limits for directory rules](https://agnostic-ai.org/docs/scoped-context/#narrow-a-rule-to-certain-files). - **Agents**: always at `.cline/agents/`, whatever the rules folder is set to. All three Cline readers use `/.cline/agents`. - `.yml` has frontmatter (between `---` lines) with the required `name` and `description`. The spec body follows as the system prompt, with no added heading. - Cline loads only `.yml` and `.yaml` files that open with `---`, and rejects an empty `name` or `description`. A missing description falls back to the name. - The generated-file comment sits inside the prompt. Above the `---` line, the file would not load. - Set `tools`, `skills`, `providerId`, `modelId`, and `maxIterations` under `x-cline`. - **Skills**: `.cline/skills//SKILL.md`, as [Cline's skills docs](https://docs.cline.bot/customization/skills) recommend. A flat file in the rules folder never loads as a skill. Frontmatter has `name` and `description`. Files next to the skill are copied unchanged. - Cline documents no field that keeps a skill user-only: any enabled skill runs from the chat input as `/`, and the model can also pick it ([Cline skills](https://docs.cline.bot/customization/skills)). Its `disabled` field hides the skill from the person too. So `disable-model-invocation` is dropped with a note, and commands are skipped. ### Old agent .md files Older releases wrote `.md` with no frontmatter, which Cline skipped. Sync removes the `.md` files it wrote. Hand-written files stay. ### Hooks Cline finds a hook script by file name only, and it has two hook runtimes. - **The VS Code extension** runs an executable named after the event with no extension, such as `.clinerules/hooks/PreToolUse` ([source](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/apps/vscode/src/core/hooks/hook-factory.ts#L1022-L1033)). It turns the SDK runtime off, so nothing runs twice ([source](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/apps/vscode/src/sdk/vscode-session-host.ts#L171-L178)). - **The SDK runtime**, which the Cline CLI runs, lists ten names (`TaskStart`, `TaskResume`, `TaskCancel`, `TaskComplete`, `TaskError`, `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionShutdown`). It scans `/.clinerules/hooks` and `/.cline/hooks` and runs every file with that name and an allowed extension (no extension counts). So sync writes one file per event to `.clinerules/hooks/`, the only layout both run, once each. The next sync removes an older `.cline/hooks/.sh` that sync wrote. Sync overwrites a hand-written `.clinerules/hooks/` for an event a spec uses. List it under `sync.unmanaged` to keep your own. A single-file `.clinerules` blocks `.clinerules/hooks/`. Sync replaces it only when a rule spec holds its content. So run `agnostic-ai import cline` first, or set `outputs.cline.hooks-dir: .cline/hooks`, which only the Cline CLI reads. Cline's docs barely cover this. The [hooks page](https://docs.cline.bot/customization/hooks) is a stub that points to SDK Plugins (a TypeScript API). Only the [config reference](https://docs.cline.bot/getting-started/config) lists hooks, under `.cline/hooks/` and in the older `.clinerules/hooks/` folder. - Each script starts with `#!/usr/bin/env bash`, and the generated-file comment follows it. The extension runs the file through its shebang, and the SDK reads the shebang to pick bash. - **`matcher` and `timeout` do nothing**, and sync says so. A hook runs on every occurrence of its event and must filter itself. Cline uses its own timeout. A [portable](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) `match` kind is the exception: the script runs the command only when the event data names one of the kind's tools, such as `run_commands` (Cline CLI) or `execute_command` (VS Code extension) for `shell`. Tool names in target overrides are matched as literal JSON values, including quotes, backslashes, and newlines. They cannot add shell commands. - Two specs on one event share one script, in spec order, and share stdout. Each spec's command runs in its own subshell under `set -e`. So a `cd`, variable, or `exit 0` in one spec does not affect the next, and a failure other than exit 2 stops the script with its code. Cline reads stdout as control JSON (the last `HOOK_CONTROL` line wins), so send anything else to stderr. - **Exit 2 blocks.** Cline ignores the exit code: only stdout `{"cancel": true}` blocks ([source](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/sdk/packages/core/src/hooks/hook-file-hooks.ts#L415-L454)). The current VS Code extension honors valid reply JSON even after a nonzero exit. So the script turns a command's `exit 2`, as on Claude Code, into a last `HOOK_CONTROL{"cancel": true, "errorMessage": ...}` line with the command's stderr, and exits 0. Any exit 2 counts, including a tool error such as `grep` on a missing file. A cancel stops the whole Cline run, not only that tool call. - **On Windows the VS Code extension runs only `.ps1`** ([source](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/apps/vscode/src/core/hooks/hook-factory.ts#L988-L997)). For a TaskStart hook with `x-cline.runtime: vscode` and `commandWindows`, a Windows sync also writes its native `.ps1` companion. Unix sync omits it so the SDK does not run a second PowerShell hook. Normal ledger cleanup removes a previously owned companion when syncing on Unix. - [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) runs a hook's script with bash, Cline's event data, and its 120 second timeout, before a session does. - `PreCompact` has no runtime event yet. Its script is written and reported but never runs. - Any other event gets a sync note instead of a file. ### Workflows With `outputs.cline.workflows-dir` set to `.clinerules/workflows`, each agent is also written as `/.md`, which you run from chat as `/.md`. An italic description, if any, comes before the body. The key is opt-in because a workflow repeats the agent in `.cline/agents/.yml`. Cline's workflows doc page now redirects to the [rules page](https://docs.cline.bot/customization/cline-rules). The [config reference](https://docs.cline.bot/getting-started/config) lists `.cline/workflows/` and the older `.clinerules/workflows/`, and Cline's resolver searches both. The VS Code extension reads only `.clinerules/workflows` and keeps it out of the rules scan, so a workflow never doubles as a rule. With `.cline/workflows`, only the CLI and SDK see them, and sync says so. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.cline.rules-dir` | `.clinerules` | set to `.cline/rules` for the other layout Cline reads; both are searched when present | | `outputs.cline.agents-dir` | `.cline/agents` | | | `outputs.cline.skills-dir` | `.cline/skills` | set to `.agents/skills` to share one folder with amp, codex, windsurf and zed; Cline scans it too | | `outputs.cline.hooks-dir` | `.clinerules/hooks` | the VS Code extension reads only `.clinerules/hooks`; the Cline CLI also reads `.cline/hooks` | | `outputs.cline.workflows-dir` | empty | opt-in; set it to `.clinerules/workflows`, the only path both hosts read | ## Global hooks `agnostic-ai sync --global --only cline` writes one executable `~/.cline/hooks/` for each event in the global hook specs. `CLINE_DIR` moves that root. Commands for the same event share one script. Shared command bodies and portable wrappers live in `~/.cline/hook-scripts/`, outside the hook discovery folder. These user hooks run in the Cline SDK and CLI. The VS Code extension reads global hooks from the system Documents folder's `Cline/Hooks/`, which sync does not write. The [config reference](https://docs.cline.bot/getting-started/config) lists both locations; the [SDK path resolver](https://github.com/cline/cline/blob/main/sdk/packages/shared/src/storage/paths.ts) and [extension loader](https://github.com/cline/cline/blob/main/apps/vscode/src/core/storage/disk.ts) show the reader difference. Global sync preserves unrelated event files and stops on a conflicting hand-written event file. Removing a global spec removes its unchanged generated script. `import --global cline` does not import hook scripts. ## Import - **Rules**: from `.clinerules/` and `.cline/rules/`. Identical duplicates become one rule. Different files with the same destination conflict. The reserved `skills`, `workflows`, and `hooks` subfolders are skipped. Native `paths` arrays are kept in `x-cline.paths`, including brace globs and empty arrays that turn off activation. Rules still follow the [filename prefix](https://agnostic-ai.org/docs/cli-reference/start/#filename-prefix-reclassification) sorting of older layouts. - **Agents**: each `.cline/agents/.yml` becomes a `.md` spec, without the generated-file header. `.yaml` is read too, and `.md` last, so files from older syncs still import. `.yml` wins a clash. The `agent-.md` prefix applies only to the old layout where rules and agents shared `.clinerules/`. - **Skills**: from the paths Cline scans, in order: `.cline/skills/`, `.clinerules/skills/`, `.claude/skills/`, `.agents/skills/`. The first same-name skill wins. Bundled assets and executable file modes are kept. The rules import skips `.clinerules/skills/`. The [config reference](https://docs.cline.bot/getting-started/config) lists `.agents/skills/`, but [the skills page](https://docs.cline.bot/customization/skills) lists only three paths, so sync keeps writing to the documented `.cline/skills`. To share one copy across tools, turn on `sync.shared-skills` or set `outputs.cline.skills-dir: .agents/skills`. ### Single-file .clinerules Cline still reads a single-file `.clinerules`. It imports as one rule, `clinerules.md`, with its `paths` kept, and the `.clinerules/skills/` lookup is skipped. Sync and `doctor --fix` then replace the file with a `.clinerules/` folder, as Cline does, but only when its content matches a rule spec. Otherwise sync, `--check`, `--dry-run`, and `doctor --fix` fail and leave it alone. Run `agnostic-ai import cline` first. ## Protected paths Not enforced. Cline takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). ## VS Code memory startup The `memory` builtin adds a project TaskStart hook that returns the existing memory indexes as `contextModification`. Cline VS Code waits up to 30 seconds, uses the workspace root as the working directory, and adds that text when the hook does not cancel. This needs Cline for VS Code 4.1.20 or later; earlier versions drop the text. The shared memory rule remains available when hooks are disabled. The default SDK and CLI TaskStart hooks remain detached and add no context. `x-cline.runtime: vscode` marks this project surface and prevents global SDK hook emission. The marker appears only in portable specs, not a vendor configuration file. On Windows, supply `commandWindows` for TaskStart to create the `.ps1` file the extension discovers. Existing hooks without this marker keep their outputs. On Unix, `agnostic-ai hook run memory-cline-task-start --target cline --runtime vscode --format json` models the VS Code TaskStart payload, executable-file discovery, timeout, and reply. It runs the synced event file, including any siblings, and excludes a shared script from expectation checks. Windows diagnostics report not run because the extension's PowerShell installation resolver is not yet modeled. Default diagnostics continue to model the SDK. Source: [VS Code hook adapter](https://github.com/cline/cline/blob/26cea2fa8bc481e942cac2f7cfb412c7c9d139f2/apps/vscode/src/sdk/hooks-adapter.ts), [hook discovery](https://github.com/cline/cline/blob/26cea2fa8bc481e942cac2f7cfb412c7c9d139f2/apps/vscode/src/core/hooks/hook-factory.ts). # Windsurf / Devin Desktop (`windsurf`) Windsurf became Devin Desktop (2026-06). The tool keeps its `windsurf` name, so existing `outputs.windsurf.*` keys and `x-windsurf` meta still work. ## First rule Install the client using the [official setup guide](https://devin.ai/desktop), then open your project in it. Select `windsurf` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.devin/rules/conventional-commits.md`. The target ID stays `windsurf`. It writes the current `.devin/` paths; Desktop and CLI differ in when scoped rules load. For existing files, check [what `import windsurf` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.devin/rules/conventional-commits.md` for the commit-prefix rule. Open the project in Devin Desktop or CLI and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Shared memory With `builtins: [memory]`, a Devin CLI `SessionStart` hook adds shared memory indexes. It needs POSIX `sh`. Native memory stays available. See [shared memory](https://agnostic-ai.org/docs/memory/#load-at-session-start) for which agnostic-ai it runs. Devin does not document the hook's default shell, working directory, timeout, or Windows execution. Local modeled checks do not establish native recall or Windows support; the memory rule remains the fallback when hooks do not run. ## Output ``` AGENTS.md # shared pointer body, plus the rules block when another tool adds it .devin/rules/.md /.devin/rules/.md # one per scoped rule .devin/agents/.md # one per agent (custom subagent profile) .agents/skills//SKILL.md # one folder per skill (shared folder) .devinignore # when ignore entries exist (indexing and agent access) .windsurfignore # when ignore entries exist (legacy name, older builds) .devin/mcp_config.json # when MCP entries exist .devin/hooks.v1.json # when hook entries exist .devin/config.json # when settings entries carry permission rules, or with repo-mode memory ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Rules Devin prefers `.devin/rules/*.md` and still reads `.windsurf/rules/` (`.windsurfrules` is legacy). Set `outputs.windsurf.rules-dir: .windsurf/rules` to keep the old layout. Otherwise sync removes the files it wrote there earlier and leaves hand-written files alone. ### Entry point Devin also reads the root `AGENTS.md`, so `sync` writes the shared instructions that point to source specs there. - When Codex or another tool that copies rules into `AGENTS.md` adds the `## Rules` block, Devin loads each always-on rule twice, because every rule also keeps its `.devin/rules/` file. - Legacy Cascade [limits](https://docs.devin.ai/desktop/cascade/memories#rules) each workspace rule file to 12,000 characters and reads `AGENTS.md` the same way. The [Devin CLI rules reference](https://docs.devin.ai/cli/extensibility/rules) gives no size limit. Sync keeps the per-rule files for compatibility. See [target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). ### Agents Custom subagent profiles ([subagents docs](https://docs.devin.ai/cli/subagents)) with `name`, `description`, `model`, `allowed-tools`, and `max-nesting`. The body is the system prompt. A scoped agent is written flat, because Devin looks in sub-folders for rules only. - `allowed-tools` maps Claude-style names to Devin's ([permissions reference](https://docs.devin.ai/cli/reference/permissions), [CLI changelog](https://docs.devin.ai/cli/changelog/stable.md)): `Read`/`Grep`/`Glob`/`Bash` to `read`/`grep`/`glob`/`exec`, and `Write`/`Edit` to `write`/`edit`. `write` needs Devin CLI v3000.11.1 or later. `mcp____` is copied as written. Anything else is dropped with a sync note. - **Devin also reads `.agents/agents/`**, flat `.md` and nested `/agent.md`. Profiles written for Antigravity, Goose, or OpenHands can therefore appear in Devin without `allowed-tools` restrictions, even when their specs exclude `windsurf`. A matching `.devin/agents/` profile can also produce a duplicate name. Check the discovered profiles; see [target behavior](https://agnostic-ai.org/docs/target-behavior/#spec-kind-behavior). - `x-windsurf.allowed-tools` writes Devin's names directly. `x-windsurf.max-nesting` sets nesting, which has no portable field. `model` is copied as written. - Devin marks custom subagents as experimental, so the format may change. ### Scoped rules Go to `/.devin/rules/.md`, because Desktop finds `.devin/rules` or `.windsurf/rules` in any sub-folder ([Desktop rules reference](https://docs.devin.ai/desktop/cascade/memories)). Devin CLI v3000.11.1 also finds rules in nested folders under `.devin/rules/` and `.windsurf/rules/` ([CLI changelog](https://docs.devin.ai/cli/changelog/stable.md)). That is unconfirmed for Desktop, so the layout stays. - With `outputs.windsurf.rules-dir` set, the prefix follows it: `/.windsurf/rules/.md`. - Devin CLI loads a sub-folder's rules once the agent touches files there. Desktop loads all of them at start. So the scope narrows what only the CLI sees. ### Rule activation A rule gets a `trigger` frontmatter key when it sets `alwaysApply: false`, or sets `globs` other than a catch-all such as `**/*` without `alwaysApply`. | Rule has | Trigger written | | --- | --- | | `globs` | `trigger: glob` (plus the pattern as written) | | `description` alone | `trigger: model_decision` | | neither | `trigger: manual` | An always-on rule has no frontmatter. Devin treats a file without frontmatter as always-on and adds its body to every message, so a `description` would do nothing. Devin's fifth value, `agent`, has no portable counterpart, so sync never writes it. ### Skills `.agents/skills//SKILL.md`. Devin reads `.agents/skills/`, `.devin/skills/`, and `.windsurf/skills/`. This adapter writes the first, so a skill is written once even when other tools also write `.agents/skills/`. Files next to the skill and file modes are kept. - `disable-model-invocation: true` is written as `triggers: [user]`, which Devin documents to "prevent the agent from invoking a skill on its own" while `/skill-name` still runs it ([Devin skills](https://docs.devin.ai/cli/extensibility/skills/overview)). So each command reaches Devin as a skill the user runs by name ([tools without commands](https://agnostic-ai.org/docs/spec-format/commands/#tools-without-commands)). When another target also writes the skills folder, the files must match, so the field is dropped with a note and commands are skipped; set `outputs.windsurf.skills-dir: .devin/skills` to keep them. Import reads `triggers: [user]` back as `disable-model-invocation: true`, so after import every selected tool treats that skill as user-only. Deleting `triggers` and importing with `--overwrite` lifts the restriction for Windsurf only when no other enabled target writes that folder. A shared folder keeps the restriction, because its files omit `triggers` during sync too. - Other native `triggers` live under `x-windsurf` in the spec and go back to top-level frontmatter on sync. This keeps `[model]` and combined invocation settings, and other tools never see a Devin-only field. An `x-windsurf.triggers` value wins over `disable-model-invocation`. ### Ignore Ignore specs are written as `.devinignore` and `.windsurfignore`, in gitignore syntax under a `#` header comment. Devin skips matched paths when indexing and cannot view, edit, or create files there ([`.devinignore` docs](https://docs.devin.ai/desktop/context-awareness/devin-ignore)). - Devin still enforces the legacy `.windsurfignore` and `.codeiumignore`. `outputs.windsurf.ignore-file: .codeiumignore` writes that name instead of `.devinignore`. - `.windsurfignore` is for older Windsurf builds, where only it blocked agent file access. Sync always writes it, unless `outputs.windsurf.ignore-file` names it. That key moves only the main file. ### MCP Servers are merged into `.devin/mcp_config.json` under `mcpServers`, which [Devin Local](https://docs.devin.ai/desktop/devin-local) reads for project scope. Cascade, with its own MCP file, was removed in Devin Desktop v3.9.19 ([changelog](https://docs.devin.ai/desktop/changelog.md), [Cascade MCP page](https://docs.devin.ai/desktop/cascade/mcp)). - [The schema](https://docs.devin.ai/cli/extensibility/mcp/configuration) uses `command`/`args`/`env` for stdio and `url`/`transport` (`http` or `sse`)/`headers`/`oauthClientId`/`oauthClientSecret`/`oauthResource` for remote. The key is `transport`, not `type`, so this tool has its own format. - Both accept `disabled`, also toggled by `devin mcp enable|disable` (see [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target)). - A `type: ws` spec writes no server and gets a sync note. Devin remote servers use only `http` and `sse`. - Cascade's personal file `~/.codeium/windsurf/mcp_config.json` is not written, since agnostic-ai writes project files only. ### Hooks Hooks are merged into `.devin/hooks.v1.json` ([hooks docs](https://docs.devin.ai/cli/extensibility/hooks/overview)). Ten events: `PreToolUse`, `PostToolUse`, `PermissionRequest`, `UserPromptSubmit`, `Stop`, `PostCompaction`, `PreCompact`, `PostCompact`, `SessionStart`, `SessionEnd`. The hooks pages still list eight; `PreCompact` and `PostCompact` come from the [v3000.11.92 changelog](https://docs.devin.ai/cli/changelog/stable.md), which says they run before and after context compaction and that a failing hook does not block it. - The file is the hooks object, `{"": [...]}`, with no `hooks` wrapper (unlike Claude Code, Codex, Gemini, and Qoder). - Per entry: `type`, `command`, and optional `timeout` (seconds). `type` is `"command"`, or `"prompt"` for an LLM prompt. A hook spec gets `"prompt"` only through a hand-written `type`/`prompt` Meta pair. - `matcher` on `PreCompact` and `PostCompact` is `manual` or `auto`, the changelog's two triggers. The built-in `handoff-pre-compact` hook does not reach Devin, because its payload is undocumented. - `matcher` is a regex on `tool_name` for `PreToolUse`, `PostToolUse`, and `PermissionRequest`. **Devin CLI names tools in lowercase snake_case** (`exec`, `edit`, `read`, `write`, `apply_patch`, `grep`, `glob`, `webfetch`, ...), so a Claude-style matcher matches nothing. Sync adds a sync note and does not rename it. - [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) runs them with Devin CLI's event data on an [assumed shell, working directory, and timeout](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#assumed-results). `--edit` is listed as not run, because Devin does not document the edit tools' input. ### Settings Portable `permissions` are merged into `.devin/config.json`, the committed project policy ([permissions reference](https://docs.devin.ai/cli/reference/permissions)). - Project configs accept only `permissions`, `read_config_from`, and `hooks` ([config file reference](https://docs.devin.ai/cli/reference/configuration/config-file)). Sync sets only `permissions`, so other keys, and other entries inside it, are kept. - In `allow`, `deny`, and `ask`, sync owns only the rules your specs write. A rule you add by hand stays, and a rule leaves with its spec. - An `x-windsurf` block on a settings spec is merged into the same file, which is how you set `read_config_from` and `hooks`. Its `permissions` are merged per spec and per list with the translated rules. - A portable `model` is not written and gets a sync note, because Devin's `agent` block works only in your personal config. - Translation: `Read(glob)` and `Write(glob)` are copied as written, `Edit(glob)` becomes `Write(glob)`, `Bash(prefix:*)` becomes `Exec(prefix)`, `WebFetch(pattern)` becomes `Fetch(pattern)`, and `mcp____` is copied as written. - Bare tool names map to Devin's own (`Bash` to `exec`, and so on), the same way as in `allowed-tools`. `WebSearch` becomes `web_search`, accepted since Devin CLI v3000.10.21 ([CLI changelog](https://docs.devin.ai/cli/changelog/stable.md)), and imports back. A bare `WebFetch` is dropped, because `webfetch` is not a permissions name. - Devin's `Exec` matches only the start of a command, so an exact `Bash(...)` rule without `:*` would match more commands. On `allow` and `ask` that would approve commands you did not list, so the rule is dropped with a sync note. On `deny` it blocks more, which is safe: `Bash(rm -rf /)` becomes `Exec(rm -rf /)`. A command deny wins over a broader `ask` or `allow` (Devin CLI v3000.10.31). - `x-windsurf.permissions` writes Devin's rules directly and replaces that spec's portable ones. - With `memory.personal: repo`, sync adds `Write(/**)` to `permissions.allow`, so Devin CLI saves to the [personal memory folder](https://agnostic-ai.org/docs/memory/#one-store-per-repository) without asking. Your own rules stay. A committed settings kind (`gitignore.commit`) keeps the rule out. A folder path with pattern characters, including parentheses, gets no rule and a note; move `AGNOSTIC_AI_HOME` or set `memory.personal: checkout`. - With `handoff` or `handoff-hook` enabled in a linked worktree, sync adds `Write(/**)` for the main checkout's ignored [handoff store](https://agnostic-ai.org/docs/handoff/#write-from-a-linked-worktree). Existing memory and user rules stay. A main checkout path with pattern characters, including parentheses, gets no rule. For a tracked, hand-written, or unmanaged file sync skips only this grant and reports the permission to add by hand (once you add it, it stays); the rest of its output is unchanged. ### Workflows `outputs.windsurf.workflows-dir` writes nothing. It only prints a warning about moving to skills. `.devin/agents/.md` is written either way. ### Older paths and removed features - `outputs.windsurf.workflows-dir` wrote each agent as a Cascade Workflow. Devin Desktop v3.9.19 removed Cascade ([changelog](https://docs.devin.ai/desktop/changelog.md)). Devin Local does not support Workflows and points users to skills ([Devin Local limitations](https://docs.devin.ai/desktop/devin-local)). - Sync removes the `.devin/rules/agent-.md` files it wrote for each current agent, and the old `.devin/rules//.md` folders. - The MCP file moved here in v3000.3 (Local 3.6). Devin moves `mcpServers` from the older `.devin/config.json` on startup, so the path works for both. - Devin CLI also reads a gitignored `.devin/mcp_config.local.json` for personal secrets, which agnostic-ai does not write. The generated `.devin/mcp_config.json` is in the managed `.gitignore` block by default (`gitignore.enabled: true`), so you do not need that split. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.windsurf.rules-dir` | `.devin/rules` | | | `outputs.windsurf.agents-dir` | `.devin/agents` | | | `outputs.windsurf.skills-dir` | `.agents/skills` | | | `outputs.windsurf.workflows-dir` | empty | when set, it only warns (see above) | | `outputs.windsurf.ignore-file` | `.devinignore` | moves only the main ignore file; the legacy `.windsurfignore` copy follows the same spec | | `outputs.windsurf.mcp-file` | `.devin/mcp_config.json` | | | `outputs.windsurf.hooks-file` | `.devin/hooks.v1.json` | | | `outputs.windsurf.conf-file` | `.devin/config.json` | project config; sync sets only `permissions` | ## Import `agnostic-ai import windsurf` reads: - **Rules**: from `outputs.windsurf.rules-dir` when set, else `.devin/rules/`, then legacy `.windsurf/rules/`, sorted by [filename prefix](https://agnostic-ai.org/docs/cli-reference/start/#filename-prefix-reclassification). Scoped copies (`//*.md`) become scoped rules again, including scopes like `.github` or `vendor`. It skips every `node_modules/`. - **Permissions**: from `.devin/config.json`. `Exec` has no exact-command form, so it imports as `Bash(:*)`, not `Bash()`. - **Skills**: from `outputs.windsurf.skills-dir` when set; otherwise `.agents/skills/`, `.devin/skills/`, then `.windsurf/skills/`, where the first same-name skill wins. Bundled assets and executable file modes are kept. `triggers` move under `x-windsurf`. - **Ignore**: from `.devinignore`, or `.windsurfignore` when the first is absent. When both files have different hand-written patterns, combine them in the imported ignore spec, in that order, before you sync. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Continue (`continue`) Continue is [no longer actively maintained](https://github.com/continuedev/continue#what-is-continue). agnostic-ai still writes its rules, skills, and MCP servers under `.continue/`, plus optional assistant files. ## First rule Install the client using the [official setup guide](https://marketplace.visualstudio.com/items?itemName=Continue.continue), then open your project in it. Select `continue` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.continue/rules/conventional-commits.md`. Agents become rule content by default. Optional assistant files are standalone configs, not automatically discovered subagents. For existing files, check [what `import continue` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.continue/rules/conventional-commits.md` for the commit-prefix rule. Open the project in Continue. Find the rule in the rules picker, then ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` .continue/rules/.md .continue/skills//SKILL.md # one folder per skill, bundled assets included .continue/mcpServers/.yaml # one per MCP entry .continue/assistants/.yaml # one per agent, only when assistants-dir is set ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rule activation**: a scoped rule matches its scope directory plus its `globs` or `paths`, and gets no `alwaysApply`. A rule with no directory scope keeps `globs`, `alwaysApply`, and `description`. A comma-separated `globs` string becomes an array, because Continue reads a string as one pattern. `x-continue.regex` works only without `scope`. See [file-pattern limits for directory rules](https://agnostic-ai.org/docs/scoped-context/#narrow-a-rule-to-certain-files). - **Skills**: `.continue/skills//SKILL.md` and bundled files are copied unchanged. The IDE extension and `cn` read this folder and `.claude/skills/`. Continue requires `name` and `description`, and offers the other files on demand ([`loadMarkdownSkills.ts`](https://github.com/continuedev/continue/blob/main/core/config/markdown/loadMarkdownSkills.ts)). It does not merge names across the two folders, so with the `claude` target each skill shows up twice. - A skill has only `name` and `description`, and every skill is offered to the model, so none can be kept user-only. `disable-model-invocation` is dropped with a note, and commands are skipped. `invokable` belongs to prompts, not skills. - **MCP**: each file is a Continue block (`name`, `version`, `schema: v1`) with the server under an `mcpServers:` list. A flat single-server file does not load. - Stdio servers get `command`/`args`/`env`/`cwd`. Remote servers get `type`/`url`/`requestOptions`, and `env` (stdio only) is dropped with a sync note. - `type: http` becomes `type: streamable-http`, the only Streamable HTTP value [Continue's schema](https://github.com/continuedev/continue/blob/main/packages/config-yaml/src/schemas/mcp/index.ts) accepts. `sse` and `streamable-http` are unchanged. `headers` moves to `requestOptions.headers`. `import continue` reverses both. - Every transport keeps `connectionTimeout` (milliseconds). Remote `requestOptions` keeps `timeout`, `verifySsl`, `caBundlePath`, `proxy`, `clientCertificate`, and `headers`. An explicit native header wins a duplicate key. `x-continue` overrides each matching top-level option. - No file is written, with a sync note, for `type: ws` (Continue has no websocket transport) or for an entry missing `command` (stdio) or `url` (remote). Either would make Continue reject the whole file. - **Assistants**: with `outputs.continue.assistants-dir` set, each agent also gets `/.yaml` with `name`, `version` (default `0.0.1`), `schema: v1`, and the body as one `prompts: [{name, description, prompt}]` entry, per Continue's schema ([reference](https://docs.continue.dev/reference)). - Continue may not scan this directory. Its [config docs](https://docs.continue.dev/guides/understanding-configs) describe one global `~/.continue/config.yaml` and no project-level agent directory. - Each file is a standalone `config.yaml` for `cn --config /.yaml` or the IDE's config picker. It has no models or rules, so your defaults apply. - The rule form (`.continue/rules/agent-.md`) is written either way. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.continue.rules-dir` | `.continue/rules` | One `.md` per rule and agent. | | `outputs.continue.skills-dir` | `.continue/skills` | | | `outputs.continue.mcp-dir` | `.continue/mcpServers` | | | `outputs.continue.assistants-dir` | empty | opt-in | ## Import `agnostic-ai import continue` reads `.continue/rules/` and sorts each file into a spec kind by [filename prefix](https://agnostic-ai.org/docs/cli-reference/start/#filename-prefix-reclassification). Native `globs` and `regex` values (strings or arrays, including empty arrays and patterns with commas) are kept under `x-continue`. - **Scope**: a subfolder named after a project directory sets the rule's scope. The rule then matches the scope directory plus its patterns. A scoped `regex` or scoped empty `globs` array is reported as unsupported. To filter by file only, omit `scope` and keep the source file outside any folder that sets scope. - **Native activation**: `x-continue.globs` and `x-continue.regex` keep the rule's activation exactly, as long as there is no portable `scope`. A rule in a subfolder stays in that folder, even when the folder names no project directory. An explicit `scope` switches to scope plus patterns, with its selector limits. - **Skills**: from `.continue/skills/` with bundled files. - **MCP**: from `.continue/mcpServers/`: `*.yaml` blocks with one server, and `.json` files (read as JSONC) with an `mcpServers` map or a single server named after the file. A duplicate or unsafe name fails before any spec is written. Connection options survive import and sync. MCP import replaces literal `env` and header values with `${NAME}` references and tells you which variables to set. Continue's `{% raw %}${{ secrets.NAME }}{% endraw %}` comes back as `${NAME}`. Sync writes Continue's secret syntax in stdio `env` and `args` and in remote `url` and `requestOptions.headers`, including `x-continue` overrides. The IDE reads secrets from a project `.env`, `.continue/.env`, or `~/.continue/.env` file. The CLI also reads process environment variables. See [Continue's secrets guide](https://docs.continue.dev/guides/configuring-models-rules-tools#working-with-secrets) and [secret resolution](https://docs.continue.dev/faqs#managing-local-secrets-and-environment-variables). Keep those files out of Git. ### Skill rules from older versions Sync removes the `.continue/rules/skill-.md` files it wrote in older versions. Import still reads a `skill-.md` rule as a skill. ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Amp (`amp`) Amp gets `AGENTS.md`, skills, MCP servers, `x-amp` settings, and orb setup from environment specs. It has no file-based agents or commands. Set `outputs.amp.agents: skill` to write agents as on-demand skills; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills). ## First rule Install the client using the [official setup guide](https://ampcode.com/docs), then open your project in it. Select `amp` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Agent prompts can become skills, but Amp has no native file-based custom agents. For existing files, check [what `import amp` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Open the project in Amp and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body (written by sync, shared by every tool that reads AGENTS.md) .agents/skills//SKILL.md # one folder per skill (Amp's native skills path) .agents/setup # executable dependency setup when an environment sets install .amp/services.yaml # supervised services when an environment sets terminals .amp/settings.json # when MCP entries or settings keys exist (merged with existing user config) ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Rules Root rules go into `AGENTS.md`. Scoped rules use `/AGENTS.md`. See [directory-specific instructions](https://agnostic-ai.org/docs/scoped-context/) for how other tools read them. ### Skills Amp's [native skills layout](https://ampcode.com/docs/customize/skills). Frontmatter carries `name` and `description`. Files next to the skill are copied as they are. Amp [replaced custom slash commands with skills](https://ampcode.com/news/slashing-custom-commands), so skills are no longer written as `.agents/commands/skill-.md`. - Amp has no user-only skill: it [removed user-invokable skills](https://ampcode.com/news/neo) on 2026-05-06, and the model decides when to load every skill. So `disable-model-invocation` is dropped with a note, and commands are skipped. - Any `x-amp` key passes through. `x-amp.mcpServers` ties MCP servers to one skill and hides their tools until it loads. Amp recommends this over user settings for most cases ([Amp skills docs](https://ampcode.com/docs/customize/skills)). A sibling `mcp.json` also works, and frontmatter wins. - Amp also loads `.claude/skills/`, `~/.claude/skills/`, and `~/.claude/plugins/cache/`. The first skill with a given `name` wins, and `.agents/skills/` comes before `.claude/skills/` ([skill order](https://ampcode.com/docs/customize/skills)). A skill synced to both `claude` and `amp` loads once. A skill for `claude` only still loads. - A skill with the same name in `~/.config/agents/skills/`, `~/.agents/skills/`, or `~/.config/amp/skills/` hides the project skill. `~/.claude/skills/` does not. - `amp.skills.disableClaudeCodeSkills` skips the Claude Code directories. `amp.skills.disableGlobalAgentsSkills` skips `~/.config/agents/skills/` and `~/.agents/skills/` ([settings schema](https://ampcode.com/cli-settings.schema.json)). Set both with `x-amp` on a settings spec. Neither has a portable field. `amp.skills.path` adds directories, separated by colons or semicolons. ### Agents No native agent output. The default writes no agent file; `outputs.amp.agents: skill` converts the prompt to a skill. Amp [removed custom commands](https://ampcode.com/news/slashing-custom-commands), so sync no longer writes `.agents/commands/.md` and removes old ones. - With `outputs.amp.rules-file` set, agent bodies reach Amp in the combined document. Otherwise Amp only gets the entry-point pointer to the source specs, and `sync` says so. - By default, agents do not go to `.agents/skills//SKILL.md`. Other tools share that tree, so an agent would duplicate the spec and overwrite a skill with the same name. - Amp's native agent API (`amp.createAgent(...)` / `amp.registerAgentMode(...)` in [plugins](https://ampcode.com/docs/customize/plugins)) works only in code. ### Commands Not supported. Commands register in plugin code via `amp.registerCommand(...)` ([Amp docs index](https://ampcode.com/llms.txt)). A command spec for amp is skipped with a warning (`on-unsupported: warn` by default). `.agents/checks/` is gone from Amp's docs, so there is no review output either. ### MCP Under `amp.mcpServers` (one dotted key, not a nested object). Stdio writes `command`, `args`, and `env`. HTTP and SSE write `url` and `headers`. Other fields go through `x-amp`. - `x-amp.includeTools` limits the tools a server exposes (tool names or glob patterns, [Amp skills docs](https://ampcode.com/docs/customize/skills)). Amp recommends fewer tools for better model performance. It stays under `x-amp` because Amp's MCP page does not mention it. - Sync changes only `amp.mcpServers`. Other keys (theme, editor settings) stay. - Workspace MCP servers need approval: the CLI asks when it first finds one, and [orbs](https://ampcode.com/docs/customize/mcp#mcp-in-orbs) ask separately. An orb approval can cover later orbs, yours or (from a workspace admin) every member's, until a synced change to that server's entry makes orbs ask again. ### Settings **Amp does not read portable settings.** Each sync reports unsupported `permissions.allow`, `permissions.deny`, `permissions.ask`, and `model` fields. Your portable deny list is not enforced. Only `x-amp` keys, in Amp's spelling, are written. - They go to `.amp/settings.json`, Amp's [workspace settings file](https://ampcode.com/docs/cli/settings). Amp finds it by searching up from the working directory to the repository root. Workspace settings override user settings, except `amp.keymap`: keymap entries in user settings override workspace ones. MCP servers and settings are merged in one write, and every other key stays. - **`allow` and `ask` cannot be written.** Amp's [settings schema](https://ampcode.com/cli-settings.schema.json) has no tool allow-list. `amp.tools.disable` only disables, and `amp.mcpPermissions` matches MCP servers, not tools. Amp [does not ask for approval by default](https://ampcode.com/docs/tools) and points to a [permissions plugin](https://ampcode.com/docs/customize/plugins#example-plugin-permissions). `model` has no key either, because Amp picks models through the Dial. - **Portable `deny` is not enforced.** `amp.tools.disable` takes native tool names, `builtin:toolname`, and `*` patterns. Run `amp tools list`, then copy the names you want to disable into `x-amp.amp.tools.disable`. Use names from your installed version; a name Amp does not recognize leaves the tool available. - A settings spec cannot set `amp.mcpServers`, because that would replace every server from MCP specs. - **The nearest file wins, with no merging.** A subdirectory with its own `.amp/` hides the root file for that subtree, for settings and `amp.mcpServers`. Delete the nested `.amp/` or copy the generated keys into it. ### Environments `install` writes an executable [`.agents/setup`](https://ampcode.com/docs/orbs/customizing), which Amp runs while preparing a project orb or snapshot. Named `terminals` write [`.amp/services.yaml`](https://ampcode.com/docs/orbs/portals): services Amp keeps running, apart from the setup script. - Each service needs `command`. Its name uses only lowercase letters, numbers, and hyphens, starts with a letter or number, and has at most 32 characters. - `x-amp.terminals` can set `cwd`, `port`, `env`, `health`, `portal`, `portals`, `review`, and `platforms`. - `platforms` is an optional list of `linux` or `darwin`. Amp skips a service that leaves out the OS it runs on. An empty list is invalid. - There is no `.agents/resume`. It runs after activation and on every wake with thread credentials, so it is neither dependency setup nor a service. - Environment specs merge by top-level field, and the last one wins. ### Old AGENT.md files Amp uses `AGENTS.md` (plural). On the first sync after upgrading, a generated `AGENT.md` at the configured root is renamed to `AGENT.md.bak`. A hand-written `AGENT.md` (no `Generated by agnostic-ai` marker) is left alone. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.amp.skills-dir` | `.agents/skills` | | | `outputs.amp.agents` | unset (agents are dropped) | `skill` writes each agent as an on-demand skill. Sync keeps it off with a note while another tool with subagents writes the same skills folder; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills) | | `outputs.amp.mcp-file` | `.amp/settings.json` | also carries settings; one file, one override | | `outputs.amp.setup-file` | `.agents/setup` | | | `outputs.amp.environment-file` | `.amp/services.yaml` | | | `outputs.amp.rules-file` | unset | writes legacy joined rules and skips the write of instructions that point to source specs | `outputs.amp.commands-dir` no longer has an effect. ## Import `agnostic-ai import amp` reads `AGENTS.md`, skills from `.agents/skills/`, and MCP servers from `.amp/settings.json`. Skill folders are restored in full, with bundled files and executable modes. Settings keys are not imported, because none has a portable field. An old `.agents/commands/` tree still imports, one agent per file. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Zed (`zed`) Zed reads `.rules`, `.agents/skills/`, `.zed/settings.json` MCP servers, and optional tasks. Set `outputs.zed.agents: skill` to write agents as on-demand skills; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills). ## First rule Install the client using the [official setup guide](https://zed.dev), then open your project in it. Select `zed` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.rules`. Hooks become optional Zed Tasks. They do not provide general before-tool enforcement. For existing files, check [what `import zed` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.rules` for the commit-prefix rule. Open the project in Zed and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` .rules # canonical entry-point pointer body + inlined rules (written by sync) .agents/skills//SKILL.md # one folder per skill (shared tree) .zed/settings.json # when MCP entries exist (merged with existing user config) .zed/tasks.json # one task per hook, only when tasks-file is set ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: Zed 1.4.2 retired its rules library, so `sync` writes the instructions that point to source specs and a marked `## Rules` block to `.rules`. - Zed reads [the first matching file](https://zed.dev/docs/ai/instructions) of `.rules`, `.cursorrules`, `.windsurfrules`, `.clinerules`, `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`. Nothing agnostic-ai writes ranks above `.rules`. `AGENTS.md` ranks below Copilot's file that only points to source specs. With copilot enabled, rules in `AGENTS.md` never reach Zed. - Zed calls `AGENTS.md` primary and `.rules` a compatibility file. If Zed drops `.rules`, the rules move back to `AGENTS.md`. - For older Zed, `outputs.zed.rules-file: .rules` replaces the instructions that point to source specs with the legacy single document (agent bodies included). - **Skills**: [Zed skills](https://zed.dev/docs/ai/skills). Identical files are written once. Sync reports `x-zed` overrides that make two copies differ. - `disable-model-invocation: true` is written to `SKILL.md`. Zed then hides the skill from the agent's catalog, and it still runs as a slash command or @-mention ([Zed skills](https://zed.dev/docs/ai/skills#disable-model-invocation)). So each command reaches Zed as a skill the user runs by name ([tools without commands](https://agnostic-ai.org/docs/spec-format/commands/#tools-without-commands)). When another target also writes the skills folder, the files must match, so the field is dropped with a note and commands are skipped. An `x-zed.disable-model-invocation` value wins. - A separate `outputs.zed.skills-dir` keeps the output apart during sync, but does not add a native search path. [Zed loads skills](https://zed.dev/docs/ai/skills#limitations) only as direct children of `~/.agents/skills/` or `/.agents/skills/`. To use another backing folder, expose it through a symlink at a documented root; sync does not create the link. - Names must be 1-64 lowercase letters or digits, with single hyphens between segments. Sync fails on names such as `Deploy`, `my_skill`, and `my--skill` and states the format. It does not rename them. - **Agents**: Zed has no per-agent support. Sync reports this unless `outputs.zed.rules-file` is set (the single document includes agent sections). - **MCP**: servers go under `context_servers` (not `mcpServers`): flat `command`/`args`/`env` for stdio, `url`/`headers` for remote (HTTP / SSE). Your other keys (theme, buffer_font_size) stay. - `disabled: true` becomes `enabled: false`. Enabled servers get no key, because Zed defaults to true. See [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target). - Set `timeout` (both transports; seconds per tool call, default 60), `oauth` (HTTP), and `remote` (stdio and extension servers) through `x-zed`. No other tool has them. - **Hooks**: with `outputs.zed.tasks-file` set, hooks become [Zed Tasks](https://zed.dev/docs/tasks) running `sh -c ""`. - `WorktreeCreate` writes `hooks: ["create_worktree"]`, so the task runs after Zed creates a linked worktree. Import restores it. Other tasks import as `OnDemand` (command palette). - Sync controls `label`, `command`, `args`, and `hooks` (adding `create_worktree` keeps other hook names). `x-zed` sets other fields, such as `cwd`, `env`, `shell`, `reveal`, `hide`, `save`, `allow_concurrent_runs`, `use_new_terminal`, `tags`, and `reevaluate_context`. ### Source for the MCP keys [Zed's MCP docs](https://zed.dev/docs/ai/mcp) do not name the `enabled` toggle. Zed's settings source, [`crates/settings_content/src/project.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings_content/src/project.rs), defines it, plus `timeout`, `oauth`, and `remote`. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.zed.skills-dir` | `.agents/skills` | | | `outputs.zed.agents` | unset (agents are dropped) | `skill` writes each agent as an on-demand skill. Sync keeps it off with a note while another tool with subagents writes the same skills folder; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills) | | `outputs.zed.mcp-file` | `.zed/settings.json` | | | `outputs.zed.tasks-file` | empty | opt-in | | `outputs.zed.rules-file` | unset | writes the legacy single document and skips the write of instructions that point to source specs, so pointing it at `.rules` replaces the pointer file instead of clashing with it | ## Global instructions `agnostic-ai sync --global --only zed` writes home instructions and rules to Zed's [personal instruction file](https://zed.dev/docs/ai/instructions). See the [global output table](https://agnostic-ai.org/docs/global-configuration/#global-output) for each platform's path. Skills still use `~/.agents/skills/`. Zed uses `~/.config/zed/AGENTS.md` on macOS, ignoring `XDG_CONFIG_HOME`. On Linux and FreeBSD, it uses `$FLATPAK_XDG_CONFIG_HOME/zed/AGENTS.md` when that variable is present, then `$XDG_CONFIG_HOME/zed/AGENTS.md` or `~/.config/zed/AGENTS.md`. Windows uses `%APPDATA%\Zed\AGENTS.md`. These roots follow [Zed's native path selection](https://github.com/zed-industries/zed/blob/ba63a022c9a22ca66d68ef2a8f0bb9b5b0a9984d/crates/paths/src/paths.rs). An empty or relative Flatpak root is rejected to avoid writing under the working directory. The first sync after a path correction clears the managed block from the old XDG file only when the ledger records it. Personal text and private permissions remain. A link to the active file is retained. An old file not recorded by sync stays untouched. `explain --global rules/.md` shows the current native file. Global instruction import is not supported; project `import zed` keeps its existing paths below. ## Import `agnostic-ai import zed` reads `.rules`, `.zed/tasks.json` (as hook specs), and MCP servers from `.zed/settings.json`. It copies `.agents/skills//SKILL.md` with all bundled files. ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Warp (`warp`) Warp reads `AGENTS.md`, `.agents/skills/`, and `.warp/.mcp.json`, plus optional workflows. Set `outputs.warp.agents: skill` to write agents as on-demand skills; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills). ## First rule Install the client using the [official setup guide](https://www.warp.dev), then open your project in it. Select `warp` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Optional agent-to-workflow output runs the agent body as a shell command. Use skills for prompt procedures. For existing files, check [what `import warp` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Open the project in Warp and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # canonical entry-point pointer body (written by sync, shared across the AGENTS.md consumers) .agents/skills//SKILL.md # one folder per skill (shared tree) .warp/workflows/.yaml # one per agent, only when workflows-dir is set .warp/.mcp.json # when MCP entries exist ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: root rules are included in `AGENTS.md`. Scoped rules go to `/AGENTS.md` ([tools that share it](https://agnostic-ai.org/docs/scoped-context/)). - **Skills**: [Warp skills](https://docs.warp.dev/agents/capabilities/skills) at `.agents/skills//SKILL.md`, the path Warp recommends. A skill in a scope folder moves under that scope, and import keeps it there. Identical root copies are written once. - Warp documents no field that keeps a skill user-only: agents invoke any skill when relevant, and `/{skill-name}` runs it by hand. So `disable-model-invocation` is dropped with a note, and commands are skipped. - Warp also scans `.warp/skills/`, `.claude/skills/`, `.codex/skills/`, `.cursor/skills/`, `.gemini/skills/`, `.copilot/skills/`, `.factory/skills/`, `.github/skills/`, and `.opencode/skills/`. agnostic-ai writes only `.agents/skills/`. Warp also picks up OpenCode's default `.opencode/skills/`, with no extra file. - `WARP_SKILL_DIRS` adds directories outside the repo for Cloud agents to index skills from. It does not change which project folders Warp scans. - **Workflows**: with `outputs.warp.workflows-dir` set, each agent becomes a [Warp Workflow](https://docs.warp.dev/terminal/entry/yaml-workflows) at `/.yaml` (`name`/`command`/`description`/`tags`). Warp now recommends Warp Drive workflows stored in the cloud, but {{path_to_git_repo}}/.warp/workflows/ still loads. - `command:` is the agent body as written, so write it as a shell snippet Warp can run. - Set `shells`, `arguments`, `source_url`, `author`, and `author_url` under `x-warp`. `import warp` reads them back. - **MCP**: `mcpServers` has only the keys in [Warp's MCP docs](https://docs.warp.dev/agents/capabilities/mcp): - Stdio: `command`/`args`/`env`, plus `working_directory` (Warp's `cwd`). `args` is required, so a server with no arguments gets `"args": []`. - Remote (HTTP/SSE/WS): `url`/`headers`, with no `type`, because Warp has no transport field. - `description`, `disabled`, and `roots` are not written. `disabled` raises a sync note (Warp never starts project servers on its own). Set `description` and `roots` through `x-warp`. - Warp also loads Claude Code's `.mcp.json`, Codex's `.codex/config.toml`, and `.agents/.mcp.json`, each after explicit approval ([provider table](https://docs.warp.dev/agents/capabilities/mcp)). With `claude` or `codex` synced too, one server can appear in up to three files, and Warp documents no handling of same-name servers. Approve only one copy. - `import warp` renames `working_directory` back to `cwd`. An entry missing `command` (stdio) or `url` (remote) is skipped, as in trae, antigravity, and windsurf. ### Upgrading from WARP.md Warp recommends `AGENTS.md` but still supports `WARP.md`, which wins in a directory holding both ([Warp rules docs](https://docs.warp.dev/agents/capabilities/rules)). The first sync after upgrading renames a `WARP.md` that agnostic-ai wrote at the configured root to `WARP.md.bak`. Sync leaves a `WARP.md` you wrote (no `Generated by agnostic-ai` marker) alone. It warns that Warp misses the synced rules until you rename or remove that file. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.warp.skills-dir` | `.agents/skills` | | | `outputs.warp.agents` | unset (agents are dropped) | `skill` writes each agent as an on-demand skill. Sync keeps it off with a note while another tool with subagents writes the same skills folder; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills) | | `outputs.warp.workflows-dir` | empty | opt-in | | `outputs.warp.mcp-file` | `.warp/.mcp.json` | | | `outputs.warp.rules-file` | unset | writes all rules into one legacy file and skips the write of instructions that point to source specs | ## Global configuration `agnostic-ai sync --global --only warp` adds your home MCP specs to `~/.warp/.mcp.json`, using the same fields as project output. The Warp app starts these global servers automatically. The Warp Agent CLI does not read this file; see [Warp app and Agent CLI](#app-and-cli). Sync leaves out a spec with `disabled: true` and explains why. `agnostic-ai import --global warp` reads this file and maps `working_directory` back to `cwd`. See [global MCP ownership and name conflicts](https://agnostic-ai.org/docs/global-configuration/#global-mcp-servers). ### Voice dictation Add [portable voice settings](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) to your home specs, then run `agnostic-ai sync --global --only warp`. Sync writes enabled and language choices into Warp's [settings file](https://docs.warp.dev/terminal/settings/): - macOS: `~/.warp/settings.toml`. - Linux: `$XDG_CONFIG_HOME/warp-terminal/settings.toml`, or `~/.config/warp-terminal/settings.toml` when unset. - Windows: `%LOCALAPPDATA%\warp\Warp\config\settings.toml`. Warp takes a lowercase two-letter language code such as `es`, or `""` for auto-detect. Sync skips broader values such as `en-US` and `auto` with a note. Warp has no capture-mode setting, so `voice.mode` also prints a note. The activation shortcut and other preferences stay in Warp. Sync preserves comments and unrelated keys in `[agents.voice]`, and removing portable voice restores the original values. Existing inline or dotted definitions of this table are left unchanged with an error asking for an explicit `[agents.voice]` table. `explain --global settings/voice.yaml` shows the file and keys for your platform. ## Warp app and Agent CLI {#app-and-cli} The Warp app and the Warp Agent CLI keep separate MCP settings ([CLI configuration](https://docs.warp.dev/agents/cli/configuration)): | Reader | Global MCP file | Project MCP file | | --- | --- | --- | | Warp app | `~/.warp/.mcp.json` | `.warp/.mcp.json` | | Warp Agent CLI | its own file (see below) | `.warp/.mcp.json`, listed as off in `/mcp` until you enable each server | - **Global servers written by sync reach the app only.** `sync --global` does not write the CLI file, so the CLI gets your rules and skills but none of your global MCP servers. To copy your app setup to the CLI once, run `/tui-migrate-setup` in the CLI. Later changes to `~/.warp/.mcp.json` do not follow. - **CLI file paths.** Warp documents only the macOS path, `~/.warp_cli/.mcp.json`. For other systems the path comes from Warp's source, not its docs: `cli/.mcp.json` inside the Warp config directory. It is unconfirmed in a running CLI; the `/mcp` header shows the file the CLI reads. - **Project servers.** Warp's CLI page says project MCP files are not detected, but the shipped CLI lists servers from `.warp/.mcp.json` in `/mcp`. They stay off until you enable each one. ## Import `agnostic-ai import warp` reads `AGENTS.md`, workflows from `.warp/workflows/`, and MCP servers from `.warp/.mcp.json`. Skills import, with all bundled files, from `/skills/` for each root Warp scans: `.agents`, `.warp`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.copilot`, `.factory`, `.github`, and `.opencode`. Within a scope, an earlier root wins a duplicate name. Scoped paths stay together. ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # CodeBuddy Code (`codebuddy`) Use `codebuddy` for Tencent's CodeBuddy Code CLI. This adapter follows the [official CLI documentation](https://www.codebuddy.ai/docs/cli/installation) and npm package `@tencent-ai/codebuddy-code` 2.177.0. Select it explicitly with `targets: [codebuddy]`. It is outside the default target list because native agent names reject digits, and scoped or empty agent tool lists cannot always be represented. ## First rule Create one rule, generate its native files, and check them: ```bash mkdir -p .agnostic-ai/rules cat > agnostic-ai.yaml <<'YAML' version: 1 targets: [codebuddy] YAML cat > .agnostic-ai/rules/commits.md <<'RULE' --- name: commits --- Use Conventional Commits. RULE agnostic-ai sync agnostic-ai sync --check ``` Open `CODEBUDDY.md` and find the rule text. CodeBuddy loads that file as project instructions. Keep editing the source rule, then sync again. ## Native files | Content | Project output | User output with `sync --global --only codebuddy` | |---|---|---| | Instructions and always-on rules | `CODEBUDDY.md` (or imported `.codebuddy/CODEBUDDY.md`) | `~/.codebuddy/CODEBUDDY.md` | | Directory-scoped rules | `/CODEBUDDY.md` | Not supported | | Conditional rules | `.codebuddy/rules/.md` | Not supported | | Agents | `.codebuddy/agents/.md` | `~/.codebuddy/agents/.md` | | Skills and bundled files | `.codebuddy/skills//SKILL.md` | `~/.codebuddy/skills//SKILL.md` | | Commands | `.codebuddy/commands/.md` | Project-only in agnostic-ai | | Settings and hooks | `.codebuddy/settings.json` | `~/.codebuddy/settings.json` | | MCP servers | Root `.mcp.json`, opt-in | `~/.codebuddy/.mcp.json` | CodeBuddy loads ancestor instructions and discovers descendant `CODEBUDDY.md` files when it works there. Its modular rules directory is loaded from the current working directory only. Conditional rules carry `alwaysApply: false` and `paths`; paths alone do not make a rule conditional. Use portable `scope` for a whole directory. A scoped rule with narrower file filters is reported as unsupported. ## Import an existing setup ```bash agnostic-ai import codebuddy agnostic-ai sync --plan agnostic-ai sync agnostic-ai sync --check agnostic-ai lint ``` Import reads root and scoped instructions, modular rules, agents, skill folders and resources, commands, settings, command and prompt hooks, and root MCP files. Native fields stay under `x-codebuddy` when they have no portable equivalent. Top-level environment and credential settings stay in the native file. Nested credentials and inline agent MCP literals fail import with a field-only diagnostic; use native environment references in supported MCP fields. Native MCP import reads `.mcp.json` before deprecated `mcp.json`. Configure the opt-in below before syncing imported project servers. Instruction frontmatter keeps its native `enabled`, `alwaysApply`, and `paths` values. `enabled: false` and `alwaysApply: false` without paths stay inactive. Import uses the source-only `x-codebuddy.instruction-file` hint, either `CODEBUDDY.md` or `.codebuddy/CODEBUDDY.md`, with validated `scope` to restore the original location. This hint is omitted from native output. If both root alternatives exist, select one with the [entry-point file configuration](https://agnostic-ai.org/docs/configuration/#entry-point-files) before import. A controlled root file cannot share its instruction policy with effective shared or local instructions, root rules, or the target overview. Sync reports that conflict before writing. Scoped native files likewise reject mixed activation policies for one path. ## Enable project MCP output ```yaml outputs: codebuddy: root-mcp-file: .mcp.json ``` Sync writes an explicit `type`: `stdio`, `http`, or `sse`. Import accepts native JSONC and infers omitted or empty types from `command` (stdio) or `url` (http). Stdio servers retain `command`, `args`, `env`, and `cwd`; remote servers retain `url` and `headers`. Both retain `description`. Native `cwd` strings stay unchanged; CodeBuddy expands uppercase `${NAME}` and `${NAME:-default}` references there. Environment references stay unexpanded by sync. `x-codebuddy.defer_loading` and `x-codebuddy.tools` keep native loading controls. Sync rejects disabled servers and remote-server `cwd`. Sync never approves project servers. CodeBuddy asks the user before their first connection. At user scope, sync and import use the first existing file: `~/.codebuddy/.mcp.json`, deprecated `~/.codebuddy/mcp.json`, then legacy `~/.codebuddy.json`. New output uses the preferred file. Comments, account state, the global `projects` map, and unmanaged servers stay intact. ## Fields and limits Agent names use lowercase letters and hyphens. Scoped tool patterns, server-only MCP names, and empty agent tool lists have no verified restrictive exposure mapping, so they fail sync. Keep scoped shell and file policies in permission rules. Use per-spec target filters when an agent requires a policy CodeBuddy cannot represent; `x-codebuddy.tools` supplies an explicit native exposure list. Agents accept `name`, `description`, `tools`, `disallowedTools`, `model`, `effort`, `skills`, `mcpServers`, `permissionMode`, `maxTurns`, `background`, `initialPrompt`, `memory`, and `disable-model-invocation`. The last field keeps a manual-only agent out of model selection. Native additions belong under `x-codebuddy`. Portable tool names map only to verified native tools; sync fails when a restriction has no mapping, preventing broader access. Native agent memory adds `Read`, `Write`, and `Edit` even to a restrictive tools list. Sync reports that widening through `on-unsupported`; `error` rejects it before writing. Skills retain native scoped allowed-tool strings such as `Bash(git:*)` and `Edit(src/**/*.ts)`, invocation controls, and native `context`, `agent`, `model`, and `hooks`. CodeBuddy applies agent, model, and hook overrides only with `context: fork`. Bundled files are copied unchanged. Commands retain description, argument hint, model, allowed tools, invocation control, and their body placeholders. Global commands are not managed by agnostic-ai, although CodeBuddy itself has a user command directory. MCP permission rules accept `mcp__server`, `mcp__server__*`, and exact `mcp__server__tool`. The all-server `mcp__*` form works in `ask` and `deny` only. Partial-segment wildcards are rejected. These permission forms follow the [native permission grammar](https://www.codebuddy.ai/docs/cli/permissions#mcp-tools). Settings map `model` to `model`, `effort` to `reasoningEffort`, permission lists to `permissions.allow/ask/deny`, and `permissions.default-mode` to `permissions.defaultMode`. Effort accepts `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Session retention maps to `cleanupPeriodDays` at user scope. Voice has no documented native key. Project `autoMode` and `permissions.defaultMode: auto` are rejected because CodeBuddy ignores them there. Command hooks retain native event names, matchers, and timeouts in seconds. Arguments are shell-quoted into the command. Prompt hooks (`type: prompt`) work on `Stop`, `UserPromptSubmit`, and `PreToolUse` in project settings, and write `prompt`, `timeout`, and `continueOnBlock`; CodeBuddy has no prompt `model` field, so sync drops it with a note. Sync rejects prompt hooks on other events, prompt hooks in user settings, and other handler types. Settings merge with existing native keys and hook entries. CodeBuddy reviews hook changes itself; frontmatter hooks also require the user's `allowUntrustedFrontmatterHooks` choice. Sync never makes trust or approval decisions. Ignore files, review configuration, environments, plugins, credentials, sessions, and automatic memories have no output in this adapter. ## Protected paths Not enforced. CodeBuddy's `protected` paths have no registered enforcement mapping in this adapter. Use native permission rules for the operations you want to deny. ## Config keys For the shared root instruction file, see [entry-point files](https://agnostic-ai.org/docs/configuration/#entry-point-files). | Output key | Default | |---|---| | `dir` under `outputs.codebuddy` | `.codebuddy` | | `outputs.codebuddy.rules-dir` | `.codebuddy/rules` | | `outputs.codebuddy.agents-dir` | `.codebuddy/agents` | | `outputs.codebuddy.skills-dir` | `.codebuddy/skills` | | `outputs.codebuddy.commands-dir` | `.codebuddy/commands` | | `outputs.codebuddy.conf-file` | `.codebuddy/settings.json` | | `outputs.codebuddy.root-mcp-file` | Unset | `dir` moves the tool directory; individual keys take precedence. Scoped instruction files keep their native names. Custom paths need CodeBuddy's own discovery configuration where they fall outside its documented paths. # OpenCode (`opencode`) OpenCode reads the root `AGENTS.md`, `.opencode/`, and `opencode.json`. ## First rule Install the client using the [official setup guide](https://opencode.ai/docs/), then open your project in it. Select `opencode` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Generated hooks use the V1 plugin API by default. Select [V2 hooks](#opencode-v2-hooks) explicitly for OpenCode V2. For existing files, check [what `import opencode` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Launch `opencode` from the project and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body + rules (written by sync) .opencode/agents/.md # one subagent per agent .opencode/skills//SKILL.md # one folder per skill, bundled assets included .opencode/commands/.md # one per command spec .opencode/commands/skill-.md # extra command form, only when emit-skills-as-commands: true .opencode/plugins/.ts # one plugin module per hook spec opencode.json # when MCP entries exist (merged with user config) ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Rules [OpenCode](https://opencode.ai/docs/rules/) finds the repo-root `AGENTS.md` by walking up from the current directory. Codex, Amp, Warp, and the other `AGENTS.md` tools share the same file, so `sync` writes it once. ### Agents ([docs](https://opencode.ai/docs/agents/)): `.opencode/agents/.md` (the singular directory is legacy), with `description`, `mode`, `model`, `temperature`, `permission`, and any `x-opencode` key. The portable `maxTurns` is written as `steps`, and `x-opencode.steps` wins. The body is the system prompt. - In legacy agent files, a `can` or `tools` list becomes the agent [`permission`](https://opencode.ai/docs/agents/#permissions) map: `can: [read]` writes `permission: {"*": deny, doom_loop: ask, external_directory: ask, read: {"*": allow, "*.env": ask, "*.env.*": ask, "*.env.example": allow}}`. Every tool not listed is denied. The catch-all comes first because the last matching rule wins. - Restricted agents keep project permission decisions for their allowed tools, including `.env` prompts, and deny other tools. Sync reads root `opencode.json`, including native V2 `permissions` in order. It does not read `opencode.jsonc`, `.opencode/opencode.json`, or global user policy, so agent rules can override restrictions kept only there. - Files with `permissions`, `request`, `system`, or `disabled` use V2 rules, including `disabled: false`. Sync writes generated restrictions as an ordered `permissions` array and appends explicit native array rules in their source order. These explicit rules can override generated restrictions. OpenCode 2 ignores a top-level `temperature` in such a file, so sync writes it as `request: {body: {temperature: ...}}`, merged with any `request` you set; a `temperature` already in `request.body` wins. OpenCode 2.2.0 keeps request values but does not yet send them with model requests ([agents](https://opencode.ai/v2/docs/agents#request)); this also applies to `temperature` in a V1 file. Import preserves the V2 fields and rebuilds the agent without its original native file. - Native `permission` keys retain source order because the last matching rule wins. `x-opencode.permission` overrides the keys it names; an explicit native action wins outright. Malformed project rules or resource rules that cannot fit the agent format fail sync with the affected paths. - V2 home-relative resources and Windows case differences can make restrictions stricter. Use absolute resources for scoped filesystem grants. Sync rejects grants whose intersection depends on the runtime home. See [native permissions](https://opencode.ai/v2/docs/permissions) for matching semantics. - These names translate: `Read`, `Edit` and `Write` (both `edit`), `Bash`, `Glob`, `Grep`, `Task`, `Skill`, `WebFetch`, `WebSearch`, and `mcp____` as `_`, and a whole server (`mcp:` or `mcp__`) as `_*`. Other names drop with a sync note. If nothing translates, no map is written, because `{"*": deny}` alone would lock the agent out. `x-opencode: {permission: {...}}` wins for each key it names; a single action such as `permission: ask` wins outright. ### Skills ([docs](https://opencode.ai/docs/skills/)): `.opencode/skills//SKILL.md` plus bundled files. OpenCode also scans `.claude/skills/` and `.agents/skills/`. A directory scope from the source folder moves the folder under that directory, and import keeps it. - `disable-model-invocation: true` keeps a skill out of the model's available list in OpenCode V2 (verified with 2.0.26). Explicit loading by ID still works. V1 does not enforce this field. Native `metadata.opencode/autoinvoke` takes precedence when both forms are set, and import preserves both. - `outputs.opencode.emit-skills-as-commands: true` also writes the command form. Its relative links to bundled files point into the skill folder (`../skills//references/x.md`). Other links stay as written. - Names must be 1-64 lowercase letters or digits, with single hyphens between segments (the vendor's `name` rule, which Zed also enforces). `Deploy`, `my_skill`, and `my--skill` fail sync with the required format. Sync does not rename them, because OpenCode V1 would skip the folder (V2 does not enforce the pattern). ### Commands `.opencode/commands/.md` keeps `description`, `agent`, `model`, and `subtask`. ### V1 hooks One [OpenCode plugin](https://opencode.ai/docs/plugins/) module per hook spec. The generated TypeScript module runs the hook command. OpenCode loads every JS or TS file in `.opencode/plugins/` at startup. - Modules follow the vendor's example: `import type { Plugin } from "@opencode-ai/plugin"`, then `export const Plugin: Plugin = async ({ $ }) => { return { ... } }`. The type-only import is removed at runtime, so `@opencode-ai/plugin` is optional. A `//` comment at the top marks the file as generated. - `PreToolUse` and `PostToolUse` map to `tool.execute.before` and `tool.execute.after`. OpenCode's dotted names are kept. Other documented events (`session.idle`, `file.edited`, `permission.asked`, ...) use the single `event` hook with an `event.type` guard. - `matcher` becomes an anchored `new RegExp("^(?:...)$")` tested against `input.tool`, so `write` does not match `todowrite`. OpenCode tool names are lowercase, so a Claude-style `Edit` or `Edit|Write` is written as is with a sync note. - Claude's `*` gets no guard. Neither does a regex that JavaScript reads differently from RE2 (`(?i)`, `\A`, `(?P`), and that case adds a note. - Bun's `$` runs each command as written. On `tool.execute.before`, exit code 2 blocks the call and stderr is the reason. Other failures log a warning, and the next command runs. This includes commands Bun's shell cannot parse (no `>&2`). - `disabled: true` writes no module, because OpenCode runs every module in the directory. - `matcher` on an event hook raises a sync note, because event data has no tool name. Any `timeout` raises one too, because OpenCode sets no time limit for handlers. - `shell.env` and `experimental.session.compacting` are declined with a note. They rewrite output that a command spec cannot express. - `import opencode` does not read `.opencode/plugins/` back. ### MCP `opencode.json` in the project root gets a `$schema` link and the `mcp` map. The linked schema marks the V1 keys sync writes (`permission`, `mcp.`) as deprecated. Sync keeps them because V1 and V2 both read them. - OpenCode V2 stores servers under `mcp.servers`. `import opencode` keeps the complete native MCP configuration in `settings/opencode-mcp.yaml`, under `x-opencode.mcp`. Sync restores it even without the original JSON file. This import and sync behavior is verified against V2 2.0.26. - Native V2 settings and portable MCP specs cannot both own the MCP configuration. Sync reports the conflicting spec paths. Use one representation for OpenCode. - Import replaces inline environment and header values with native environment references. It refuses credentials it cannot rewrite, including inline OAuth secrets. Use `{env:NAME}` references before importing those fields. - Sync manages each native server separately. Servers and defaults added by hand stay; a removed spec server stays when you have edited it. - Stdio becomes `{type: "local", command: [...], cwd}`, and HTTP or SSE becomes `{type: "remote", url, headers}`. `cwd` (relative to the workspace) keeps its name. - `timeout` keeps its name and uses milliseconds for MCP requests, including tool calls. OpenCode defaults to `30000` ms when connecting and fetching tools. - `disabled: true` writes `"enabled": false` ([MCP docs](https://opencode.ai/docs/mcp-servers/)), and import reads `enabled: false` back. An enabled server gets no key. - Other documented fields, such as the `oauth` object for pre-registered remote servers, go through `x-opencode`. - Sync changes only `$schema` and `mcp`. `theme`, `model`, and other keys stay. They never show as drift in `sync --check` or `doctor`, and `doctor --fix` keeps them. ### Settings A settings spec's default `model` merges into `opencode.json`, and import restores it to `settings/opencode.yaml`. An `x-opencode` block merges in too. Its `permission` merges tool by tool with the translated rules. OpenCode 2.2.0 sets a model's prompt cache lifetime under `provider..models..settings.cache.ttl` ([config schema](https://opencode.ai/config.json)): a number followed by `ms`, `s`, `m`, `h`, or `d`. No other target has this setting, so write it under `x-opencode.provider`: ```yaml target: opencode x-opencode: provider: anthropic: models: claude-sonnet-4-5: settings: cache: ttl: 1h ``` ### Permissions [OpenCode's V2 migration guide](https://opencode.ai/v2/docs/migrate-v1#permissions-and-tools) distinguishes the defaults: V1 asks before accessing paths outside the project; V2 allows `external_directory` by default. To request approval in V2, add the rule below to your existing `x-opencode.permissions` array, keeping all its rules and their order. If your native `permissions` array exists only in `opencode.json`, move the full array into one settings spec under `x-opencode.permissions` before adding the rule. Only when neither location has a V2 policy, save this settings spec as `.agnostic-ai/settings/outside-project.yaml`: ```yaml targets: [opencode] x-opencode: permissions: - action: external_directory resource: "*" effect: ask ``` Keep the native `permissions` array in one settings spec: arrays from separate specs replace each other rather than combine. Put this broad `ask` rule before intentional exceptions. [V2 permissions](https://opencode.ai/v2/docs/permissions#directories) use the last matching rule. Portable `allow`, `deny`, and `ask` lists become [OpenCode's `permission` map](https://opencode.ai/docs/permissions/). - A bare tool name covers the whole tool (`Read` becomes `read: allow`). A scoped rule becomes a glob (`Bash(go test:*)` becomes `bash: {"go test *": "allow"}`). - `Write` and `Edit` both land on `edit`, which covers edit, write, and patch. - If two lists claim one tool and pattern, the stricter wins. - OpenCode takes the last match, so inside one tool sync writes allow, then ask, then deny, each group with the `*` catch-all first. A narrower allow never bypasses a broader deny, as on Claude Code. - `mcp____` becomes `_` (`mcp__github__create_issue` becomes `github_create_issue: deny`), the key OpenCode uses for MCP tools ([agents docs](https://opencode.ai/docs/agents/)). The server name keeps its hyphens. - Scoped `WebFetch` or `WebSearch` rules (those keys take a bare action) and denying a whole MCP server (`github_*` has no portable spelling) raise a sync note. - `x-opencode.permission` writes OpenCode's own map and replaces the translated rules for that tool. - Sync owns only the tool keys your specs produce. A key you wrote in `permission` that no spec sets, such as `"edit": "ask"`, stays, and a bare `"permission": "ask"` stays as the `"*"` entry. A tool key leaves with its spec while you have not edited it. When a spec starts setting a key you wrote, sync saves the file as `opencode.json.bak` first. - A project synced before this change has one record for the whole `permission` value. If you edit another key in the same sync where a spec drops a tool, that tool stays as yours, since sync kept only a checksum of what it wrote. - `import opencode` reads the map back, except namespaced MCP keys, where the server name cannot be told from the tool name. ### Old .opencode/AGENTS.md leftovers Sync removes a generated file at the old `.opencode/AGENTS.md` path, which OpenCode never read. It leaves a hand-written one. ## OpenCode V2 hooks Set `x-opencode.plugin-api: v2` on each hook for OpenCode V2. This route is verified against 2.0.26. Omitting it, or choosing `v1`, keeps the V1 plugin format. Other values fail sync with the source path. ```yaml name: guard-shell targets: [opencode] event: PreToolUse matcher: shell command: .agnostic-ai/scripts/guard-shell.sh x-opencode: plugin-api: v2 ``` On macOS and Linux, V2 supports `PreToolUse`, `PostToolUse`, `tool.execute.before`, and `tool.execute.after`. Other events fail sync. Use these native event names; OpenCode has no portable `on` mapping. Explicit matchers stay anchored JavaScript regular expressions: `bash` is not rewritten to `shell`. An invalid expression prevents the plugin from loading. The plugin registers a native [V2 tool callback](https://opencode.ai/v2/docs/build/plugins) and runs commands in the triggering session's checkout. It sends the native event as one JSON object plus a newline on stdin. Before-tool input includes `tool`, `sessionID`, `agent`, `messageID`, `id`, and `input`. After-tool input adds `status` and either `result` or `error`. `PostToolUse` runs only for completed tools; the native `tool.execute.after` event also runs for failed tools. Commands run in order. A before-tool command exiting 2 blocks the operation and stops later commands; stderr gives the reason. Other command failures log the hook name, command, and error, then continue. Stdout is ignored. Structured stdout decisions are not supported by this route. With `args`, the executable and each argument are passed separately. Without `args`, the command runs through `/bin/sh`. Exec-form scripts must be executable. Shared scripts are copied to `.opencode/hooks/` as with V1. On Windows, use OpenCode V1 with the default V1 hooks. OpenCode V2 cannot run V1 plugins. V2 plugins load with a clear warning and refuse matching callbacks before reading session data or starting a command. A before-tool callback blocks the operation. An after-tool callback reports the refusal after the operation has already run. `timeout` is a positive whole number of seconds, up to 86400; the default is 60 seconds. A timeout stops the owned process tree and logs a failure. Unloading the plugin also stops active commands and removes its callbacks. `x-opencode.env` adds string values to each command's inherited environment. Each invocation receives `AGNOSTIC_AI_TARGET=opencode`. Sync writes `.opencode/plugins/.ts`. The type-only `@opencode/plugin` import adds no runtime dependency. Arbitrary plugin code is not imported back into specs. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.opencode.agents-dir` | `.opencode/agents` | | | `outputs.opencode.skills-dir` | `.opencode/skills` | | | `outputs.opencode.commands-dir` | `.opencode/commands` | | | `outputs.opencode.hooks-dir` | `.opencode/plugins` | OpenCode only loads plugins from its own directory, so moving this stops the hooks loading | | `outputs.opencode.mcp-file` | `opencode.json` | | | `outputs.opencode.emit-skills-as-commands` | `false` | | | `outputs.opencode.rules-file` | unset | writes legacy joined rules and skips the write of instructions that point to source specs | With `builtins: [memory]`, sync adds both [shared memory](https://agnostic-ai.org/docs/memory/) indexes to `instructions` and keeps your own entries. Dropping the built-in removes only those two. With `memory.personal: repo`, sync also adds `"/**": "allow"` to `permission.external_directory`, so OpenCode saves to the [personal memory folder](https://agnostic-ai.org/docs/memory/#one-store-per-repository) without asking. Your own entries stay in the order you wrote them, and the folder entry goes last when sync first adds it, since OpenCode applies the last rule that matches. A bare action you set there, such as `"deny"`, becomes the `"*"` entry. A bare `"permission": "deny"` becomes `{"*": "deny"}`, which means the same and stays after repo mode ends. Once sync has added the folder entry it stays where it is, so a broader rule you put after it keeps winning, and an entry you edit stays yours. A folder path with `*` or `?`, which OpenCode reads as wildcards, gets no `external_directory` entry and a note; move `AGNOSTIC_AI_HOME` or set `memory.personal: checkout`. With `handoff` or `handoff-hook` enabled in a linked worktree, sync also allows the main checkout's ignored [handoff store](https://agnostic-ai.org/docs/handoff/#write-from-a-linked-worktree) under `permission.external_directory`. Existing memory and user permission rules stay, and a rule you or a spec wrote for that path, such as `deny` or `ask`, is never replaced. A main checkout path with `*` or `?` gets no grant. For a tracked, hand-written, or unmanaged file sync skips only this grant and reports the permission to add by hand (once you add it, it stays); the rest of its output is unchanged. ## Import `agnostic-ai import opencode` reads: - **Instructions and rules**: root `AGENTS.md`, falling back to legacy `.opencode/AGENTS.md` when the root file is absent. The shared body becomes `.agnostic-ai/AGNOSTIC_AI.md`. Rule sections that sync wrote become rule specs. Hand-written instructions stay whole. - **Agents and commands**: top-level Markdown files in `.opencode/agents/` and `.opencode/commands/`. - **Skills**: `.opencode/skills/`, `.claude/skills/`, then `.agents/skills/` (first wins within a scope), with scoped paths and bundled files. - **MCP servers**: the `mcp` map in `opencode.json`. - **Settings and permissions**: the default `model` becomes `settings/opencode.yaml`; portable entries in the `permission` map become `settings/permissions-opencode.yaml`. Namespaced MCP permission keys have no portable spelling and stay in the file. - **Providers**: the `provider` block, such as a model's cache lifetime, becomes `settings/opencode-provider.yaml` under `x-opencode.provider`. `{env:NAME}` and `{file:path}` references import as written. A block that holds a literal credential, such as an inline `apiKey`, is left out with a warning and stays in `opencode.json`. Import fails when another settings spec sets a different `x-opencode.provider`. Plugin hooks in `.opencode/plugins/` are not imported. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Google Antigravity (`antigravity`) Antigravity reads per-rule files under `.agents/rules/` and custom subagents under `.agents/agents/`. agnostic-ai writes both, plus skills, MCP servers, and hooks. ## First rule Install the client using the [official setup guide](https://antigravity.google/download), then open your project in it. Select `antigravity` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.agents/rules/conventional-commits.md`. Rules need native trigger frontmatter. Sync supplies it from the portable activation fields. For existing files, check [what `import antigravity` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.agents/rules/conventional-commits.md` for the commit-prefix rule. Open the project in Antigravity. Confirm the rule is Always On, then ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Shared memory With `builtins: [memory]`, sync writes `antigravity-memory-index.md` as an always-on rule. Native `@[label](path)` includes load the project index and checkout personal index. Paths resolve from the generated rule file, including when `outputs.antigravity.rules-dir` changes. The rule stays identical whether either index exists. Sync reads existing indexes to check the expanded rule size and does not create empty ones. With `memory.personal: repo` in local configuration, only project memory is included. The shared policy finds personal memory through `agnostic-ai memory path`. Global sync keeps that policy and omits project-file includes. Antigravity expands includes before checking its 24,000-byte rule limit. All always-on rules share a 20,000-token budget, which can replace large rules with pointers. Missing-index behavior is not documented. See the [official file include and size rules](https://www.antigravity.google/docs/rules/). ## Output ``` .agents/AGENTS.md # entry-point file with a short pointer body .agents/rules/.md # one per rule /.agents/rules/.md # one per scoped rule .agents/agents//agent.md # one per agent (custom subagent) .agents/skills//SKILL.md # one folder per skill (Antigravity's native path) .agents/mcp_config.json # when MCP entries exist .agents/hooks.json # when hook entries exist .agents/plugins//plugin.json # only when an output key points into a plugin ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Entry point `.agents/AGENTS.md` is the [documented per-directory entry point](https://antigravity.google/docs/rules) (`/.agents/AGENTS.md` or `/.agents/GEMINI.md`, both `always_on` and frontmatter-free). It stays apart from the root `AGENTS.md` that Codex, Amp, and Warp use. ### Paths Rules, skills, and MCP go to the plural `.agents/` form Antigravity prefers ([rules](https://antigravity.google/docs/rules), [skills](https://antigravity.google/docs/skills?tab=ide)). Antigravity lists the singular paths as legacy. ### Plugins Point any output folder or file key at `.agents/plugins//` to ship those specs as a [workspace plugin](https://antigravity.google/docs/plugins), active only in that project. agnostic-ai writes the required `plugin.json` at the plugin root, one per plugin, when an output key points at a component of the [plugin layout](https://antigravity.google/docs/build-plugins#supported-components): `skills/`, `agents/`, `rules/`, `hooks.json`, or `mcp.json`. A path inside a component, such as `.agents/plugins/team/skills/extra`, gets none. To bundle MCP servers in a plugin, set `outputs.antigravity.mcp-file` to `.agents/plugins//mcp.json`, the path Antigravity documents for [plugin MCP](https://antigravity.google/docs/mcp?tab=ide). A legacy output at `.agents/plugins//mcp_config.json` still gets its manifest. Sync prints a note to move to `mcp.json`. Whether Antigravity loads the legacy name is unproven. The standalone file stays at `.agents/mcp_config.json`. Sync keeps an existing `plugin.json` it did not write and prints a note. Manifests recorded by an earlier sync remain managed. ### Rules Every file starts with frontmatter declaring a `trigger`. Antigravity [silently discards](https://antigravity.google/docs/rules) a file without one or with an unknown value. The generated-file header follows the closing `---`. - `alwaysApply` is checked first, as in [Devin Desktop / windsurf](https://agnostic-ai.org/docs/targets/windsurf/) and Cursor. `true` writes `trigger: always_on` and ignores `globs`, so the rule that `new rule` creates (`globs: "**/*"` plus `alwaysApply: true`) stays always-on. When unset, it counts as `false` if `globs` holds something other than a catch-all such as `**/*`, and as `true` otherwise. - With `alwaysApply: false` (or `globs` and no `alwaysApply`), `globs` becomes `trigger: glob` plus a quoted, comma-joined `globs`. A `description` alone becomes `trigger: model_decision`. A rule with neither becomes `trigger: manual` (loads only on an @-mention). The Antigravity CLI [does not support rule mentions](https://antigravity.google/docs/rules), so a `manual` rule does not load there. Each case has the field it needs, so no sync note appears. `description` is kept on any trigger. - **`x-antigravity.trigger` overrides the mapping.** `import antigravity` writes it, because the portable fields cannot always recover the trigger. A `manual` rule with a `description` would come back as `model_decision`. An override missing its field (`globs` for `glob`, `description` for `model_decision`), or outside `always_on`/`glob`/`model_decision`/`manual`, becomes `trigger: manual` with a sync note, never `always_on`. - **24,000-byte cap.** Antigravity [truncates](https://antigravity.google/docs/rules) a rule file over 24,000 bytes after expanding `@[label](path)` includes. agnostic-ai writes it in full and adds a sync note, so split the rule. The count covers the written file (frontmatter, generated-file header, heading) and expands the built-in memory rule's project and checkout personal indexes, stripping included frontmatter. Other rules' includes are not counted. - **20,000-token budget.** All `always_on` rules share 20,000 tokens. Past it, Antigravity replaces its largest rules with a `path: description` pointer. `sync` gives no note, so give large rules a `description`. - **Scoped rules**: `scope: ` writes to `/.agents/rules/.md` with the same frontmatter. Antigravity walks up from each file it reads or edits and [loads rules at each level](https://antigravity.google/docs/rules). It scans only the `.md` files directly inside `.agents/rules/`, so rules never nest deeper. ### Agents One custom subagent at `.agents/agents//agent.md`, the nested form in [Antigravity's subagent reference](https://antigravity.google/docs/subagents) (a flat form also exists). Frontmatter has the required `name` and `description`. The body is the system prompt. - Goose and OpenHands scan only top-level `.md` files in that folder. The nested form keeps Antigravity's `model` tier apart from their model IDs. `import antigravity` reads flat files only when no nested profile exists, so it skips their agents. - **Devin reads this folder too.** With `windsurf` in `targets`, Devin finds this profile and `.devin/agents/.md` under one name, and only the Devin copy has `allowed-tools`. See the [shared agent discovery](https://agnostic-ai.org/docs/target-behavior/#spec-kind-behavior). - A portable `tools` list is dropped with a sync note. Antigravity has its own names (`view_file`, `replace_file_content`, `grep_search`, `run_command`, ...), and an unknown one can hang the subagent. Set `x-antigravity.tools` instead. - `model` is a tier (`inherit`, `flash`, `pro`), so any other value is dropped the same way. - Other documented keys (`mainAgent`, `subagent`, `commandExecutionPolicy`, `mcpServers`, `skills`/`plugins`) go under `x-antigravity`. ### Skills Antigravity's [native skills layout](https://codelabs.developers.google.com/getting-started-with-antigravity-skills), shared with every tool that writes `.agents/skills/` (see [shared skills](https://agnostic-ai.org/docs/target-behavior/)), so identical folders are written once. Frontmatter is cut to `name` and `description`. Files next to the skill are copied unchanged. `sync --global` writes `~/.gemini/config/skills//`, the [global path](https://antigravity.google/docs/skills?tab=ide) for the IDE and Antigravity app. Antigravity documents no field that keeps a skill user-only: the agent reads relevant skills on its own, and `/` runs one by hand ([Antigravity skills](https://antigravity.google/docs/skills)). Its `disable-slash-command: true` does the opposite, leaving the skill to the model. So `disable-model-invocation` is dropped with a note, and commands are skipped. ### MCP One `mcpServers` object in `.agents/mcp_config.json` ([docs](https://antigravity.google/docs/mcp?tab=ide)). - agnostic-ai writes remote servers with `serverUrl`. Antigravity also accepts `url` as an alias; `httpUrl` is unsupported. stdio servers have `command`, `args`, `env`, and `cwd`. Remote ones add `headers`. - Both kinds accept `disabled` by that name (see [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target)), unlike Codex and Kilo, which write `enabled: false`. - `authProviderType`, `oauth`, `disabledTools`, `description`, `roots`, and any new field go under `x-antigravity`, like Zed and Warp. ### Hooks Hooks are merged into `.agents/hooks.json` (or `outputs.antigravity.hooks-file`), the [documented location](https://antigravity.google/docs/hooks?tab=ide). - **Keyed by definition name, not event**, unlike every other tool with hooks. Each spec becomes a top-level definition named after it, holding its one event. `disabled: true` writes `enabled: false` beside the event key, the reverse of MCP's literal `disabled`. - Six events: `PreToolUse`, `PostToolUse`, `SessionStart`, `PreInvocation`, `PostInvocation`, `Stop`. Other events are skipped with a sync note. The first two hold `{matcher, hooks: [...]}` groups. The other four hold a plain handler list and ignore the matcher, which gets a note. `SessionStart` runs once when a conversation starts, before the first model call. The portable `on: session-start` is not mapped; set `event: SessionStart` or `x-antigravity: {event: SessionStart}`. - Per handler: `type` (defaults to `"command"`, and is always written), `command`, and optional `timeout` (seconds, vendor default 30). - A Claude-style matcher is valid regex that matches none of Antigravity's tool names. It is written as is with a sync note, as in OpenHands, Crush, and Windsurf. ### Commands The public-preview docs do not confirm them, so they are skipped with a warning. `on-unsupported: silent` suppresses it. ### Leftovers from older versions - Older versions wrote the undocumented `.agent/AGENTS.md`. Sync renames a file it wrote there to `.agent/AGENTS.md.bak`. - Sync removes the `.agent/rules` and `.agent/skills` files it wrote, unless `outputs.antigravity.rules-dir` / `skills-dir` opts back into the legacy path. - Agents used to be flattened into `.agents/rules/agent-.md`, which the subagent loader does not read. Sync removes the copy it wrote for every current agent. - Sync removes a flat agent profile it wrote when no enabled tool still writes it. With Goose or OpenHands enabled, it stays as their output. - The IDE still loads `~/.gemini/antigravity/skills/`, which older versions wrote, but the Antigravity app does not. Move those skills or re-run `sync --global`. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.antigravity.rules-dir` | `.agents/rules` | | | `outputs.antigravity.agents-dir` | `.agents/agents` | | | `outputs.antigravity.skills-dir` | `.agents/skills` | | | `outputs.antigravity.mcp-file` | `.agents/mcp_config.json` | | | `outputs.antigravity.hooks-file` | `.agents/hooks.json` | | | `outputs.antigravity.rules-file` | unset | writes one legacy merged file and skips the instructions that point to source specs | ## Global configuration `agnostic-ai sync --global --only antigravity` adds your home MCP specs to `~/.gemini/config/mcp_config.json`, using the same fields as project output. A disabled server keeps `disabled: true`. `agnostic-ai import --global antigravity` reads this file, maps `serverUrl` back to `url`, and keeps extra fields under `x-antigravity`. See [global MCP ownership and name conflicts](https://agnostic-ai.org/docs/global-configuration/#global-mcp-servers). ## Import `agnostic-ai import antigravity` reads `.agents/rules`, `.agents/skills/` (with bundled files), and `.agents/AGENTS.md`. It falls back to `.agent/rules`, `.agent/skills/`, and `.agent/AGENTS.md` only when the preferred path is absent. It also looks for `/.agents/rules/` copies to rebuild scoped rules. - **Triggers**: `trigger` becomes `alwaysApply` (`always_on` to `true`, `glob`/`model_decision`/`manual` to `false`). It is also kept under `x-antigravity.trigger`, which `sync` reads first, so a `manual` rule with a `description` stays `manual`. An unknown `trigger` gets no `alwaysApply` and a warning, and `sync` writes it as `manual`, never `always_on`. - **Globs**: `globs` is kept, and the singular `glob:` is merged into it. - **Agents and MCP**: nested profiles win over flat files. `serverUrl` becomes `url`, and other fields are kept under `x-antigravity`. - **Plugin files**: import reads the configured `outputs.antigravity.mcp-file`, `agents-dir`, and `rules-dir`. These paths can point into `.agents/plugins//`. Import does not use `skills-dir`. Skills come from `.agents/skills/`, with `.agent/skills/` as a fallback. Import skips everything else under `.agents/plugins/`. Absolute MCP file paths must resolve inside the project and outside `.git`. Import previews use the same boundary through project links and Windows short paths; a source folder alias does not grant access to an outside MCP file. ## Protected paths Not enforced. Antigravity takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Junie (`junie`) JetBrains Junie reads `.junie/AGENTS.md` (instructions that point to source specs and rules), the rest of `.junie/`, and `.aiignore`. ## First rule Install the client using the [official setup guide](https://junie.jetbrains.com/docs/), then open your project in it. Select `junie` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.junie/AGENTS.md`. Instructions apply to the IDE and CLI. Native subagents and slash commands on this page are CLI features. For existing files, check [what `import junie` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.junie/AGENTS.md` for the commit-prefix rule. Open the project in Junie and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # root pointer body (shared path; fallback) .junie/AGENTS.md # preferred entry point: pointer body + rules .junie/agents/.md # one file per subagent .junie/skills//SKILL.md # one folder per skill, plus any bundled assets .junie/commands/.md # one file per slash command .junie/mcp/mcp.json # when MCP entries exist .junie/config.json # when a settings spec selects a default model or effort .aiignore # when ignore entries exist ``` ## Native mappings ### Field mappings, discovery, and compatibility Junie uses the first guidelines source it finds, with no merging ([IDE plugin](https://junie.jetbrains.com/docs/junie-ide-plugin.html), [guidelines](https://junie.jetbrains.com/docs/guidelines-and-memory.html)): 1. `.junie/AGENTS.md` (preferred). 2. The root `AGENTS.md` plus `.junie/playbook.md` and every `.junie/rules/*.md`, when `.junie/` has nothing. 3. The legacy `.junie/guidelines.md` / `.junie/guidelines/`. `sync` always writes `.junie/AGENTS.md`, so Junie [uses it exclusively](https://junie.jetbrains.com/docs/environment-variables.html) and step 2 never applies. ### Rules A `## Rules` block right after the instructions that point to source specs, marked so sync can find it. Each rule is a `### ` section (source comment, optional description, full body), the same shape every tool that includes rules in instruction files uses. ### Agents ([docs](https://junie.jetbrains.com/docs/junie-cli-subagents.html)): Junie reads `.junie/agents/` (the vendor's preferred location and the default here) or `.agents/`. Junie CLI offers to import `.cursor/agents/`, `.claude/agents/`, and `.codex/agents/` into it. Set `outputs.junie.agents-dir: .agents` for the shared alternative, like Codex's `outputs.codex.agents-dir: .agents/agents`. - Names, and filenames when `name` is missing, must match `[a-z][a-z0-9_-]*` (underscores allowed, unlike the OpenCode and Zed skill rule). An invalid name such as `Deploy_Bot` fails sync, and the error shows the required format. Sync does not rename it. - Frontmatter is copied as written. The documented fields (`name`, `description`, `tools`, `disallowedTools`, `mcpServers`, `model`, `permissionMode`, `reasoningLevel`, `maxTurns`, `skills`, `allowPromptArgument`) use the same spelling as specs, and `tools` and `disallowedTools` stay YAML lists. - Tool group labels match the Claude names for each documented group, plus `AskUserQuestion`. There is no `WebFetch`, `Task`, `TodoWrite`, or `NotebookEdit`. - `reasoningLevel` also accepts the alias `effort`, which wins when both are set. Both are copied as written. - Agent bodies are not copied into `.junie/AGENTS.md`, as with Augment and Kilo Code. - Early Access covers only the automatic model-selection toggle (`/settings → Subagents`), not the file format or how Junie finds agents. ### Commands ([project commands folder](https://junie.jetbrains.com/docs/custom-slash-commands.html)): Junie documents `description` and `allowPromptArgument` (free-form text through a `$prompt` placeholder). Other keys are copied as written. Junie fills `$argumentName` placeholders in the body when you run the command. ### CLI only The IDE plugin docs mention neither subagents nor slash commands. ### Skills Bundled files are copied as they are, and a flat file never loads as a skill. Junie CLI also loads `.agents/skills/` in a trusted project ([agent-skills](https://junie.jetbrains.com/docs/agent-skills.html)), so another tool that writes it gives Junie a harmless second copy. ### MCP `.junie/mcp/mcp.json` in the standard `mcpServers` schema (`command`/`args`/`env` local, `url`/`headers` remote). The per-server `timeout` is also preserved in milliseconds for local and remote servers. `x-junie.timeout` overrides the portable value. Junie defaults to 60000 ms and documents a range of 1000 through 3600000 ms ([request timeout](https://junie.jetbrains.com/docs/junie-cli-mcp-configuration.html#request-timeout)). `disabled` and `description` are dropped with a sync note. Junie enables imported servers by default. Disable one with `/mcp`, then **→ Disable**. See [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target). ### Settings A settings spec's default `model` and `effort` merge into `.junie/config.json`, keeping other keys. Junie loads this file only in a trusted project. `effort` passes through as written, and `x-junie.effort` overrides it ([effort by target](https://agnostic-ai.org/docs/spec-format/settings/#effort-by-target)). `x-junie` adds project-config keys that have no portable field. ### Ignore `.aiignore` in the project root, in `.gitignore` syntax ([IDE plugin](https://junie.jetbrains.com/docs/junie-ide-plugin.html)). Specs are joined. Override the path with `outputs.junie.ignore-file`. - Junie asks before it views or edits a listed file instead of blocking it, and it protects contents, not names. - Brave Mode, or an allowlisted command that names the path, skips the prompt. - Only the IDE plugin docs mention `.aiignore`. No CLI page does. With `sync.target-overview` off and another tool that reads `AGENTS.md` enabled, `.junie/AGENTS.md` and the root `AGENTS.md` are identical. ### IDE plugin Custom path The IDE plugin reads an optional **Custom path** (Settings | Tools | Junie | Project Settings) before `.junie/AGENTS.md`. The CLI does not, and the setting is rarely committed, so it seldom changes which file wins. ### Files from older versions Older versions wrote rules and agents to `.junie/rules/`, which `.junie/AGENTS.md` hides. Sync removes generated leftovers there and keeps hand-written files. It also drops an older `## Agents` block, because it rewrites `.junie/AGENTS.md` in full. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.junie.agents-dir` | `.junie/agents` | | | `outputs.junie.skills-dir` | `.junie/skills` | | | `outputs.junie.commands-dir` | `.junie/commands` | | | `outputs.junie.mcp-file` | `.junie/mcp/mcp.json` | | | `outputs.junie.ignore-file` | `.aiignore` | | | `outputs.junie.rules-dir` | `.junie/rules` | legacy: only changes where leftover files are removed, for projects that set it | `.junie/AGENTS.md` is a fixed path. ## Import `agnostic-ai import junie` reads: - The Rules block in `.junie/AGENTS.md`. - Agents from `.junie/agents/.md` (or `.agents/.md`). - Commands from `.junie/commands/.md`. - Skills, with bundled files, from `.junie/skills//SKILL.md`, then `.agents/skills/` for names not already found. - The default `model` and `effort` in `.junie/config.json`, restored to `settings/junie.yaml`. - MCP servers from `.junie/mcp/mcp.json`, including request timeouts. Older layouts still import: - With no native agent files, the older Agents block in `.junie/AGENTS.md`. - `.junie/rules/`, when it exists, wins over `.junie/AGENTS.md`. Each file is sorted by [filename prefix](https://agnostic-ai.org/docs/cli-reference/start/#filename-prefix-reclassification). - A legacy flat `.junie/rules/skill-.md` imports as a skill. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Kiro (`kiro`) agnostic-ai writes AWS Kiro steering files, native skills, agents, commands, and hooks under `.kiro/`, plus MCP servers and `.kiroignore`. ## First rule Install the client using the [official setup guide](https://kiro.dev), then open your project in it. Select `kiro` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.kiro/steering/conventional-commits.md`, or shared `AGENTS.md` when another selected target already puts the rule there. File-matching steering rules need the IDE or CLI V3. Earlier CLI versions load only always-on rules. For existing files, check [what `import kiro` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and find the commit-prefix rule in `.kiro/steering/conventional-commits.md` or the shared `AGENTS.md`. Open the project in Kiro. Confirm the steering rule or shared instructions are loaded, then ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body, plus the rules block when another tool adds it .kiro/steering/.md # one per rule (inclusion: always, or fileMatch + fileMatchPattern from globs) .kiro/skills//SKILL.md # one per skill (+ bundled scripts/, references/, assets/) .kiro/agents/.md # one per agent .kiro/prompts/.md # one per command (slash commands in CLI V3) .kiro/hooks/.json # one per hook .kiro/settings/mcp.json # when MCP entries exist .kiroignore # when ignore entries exist ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: [steering files](https://kiro.dev/docs/steering/) must start with their YAML frontmatter. Rules without globs use `inclusion: always`. Rules with globs use `fileMatch` with `fileMatchPattern`, a string for one pattern and a list for several. Rules with globs need the Kiro IDE or Kiro CLI V3. CLI V1 and V2 load only `always` files. Sync never writes Kiro's `auto` and `manual` modes. - **`AGENTS.md`**: Kiro includes it in default resources. When Codex or another tool that copies rules into `AGENTS.md` adds the `## Rules` block, always-on rules normally get no steering file. `fileMatch` rules keep theirs. [Custom agents inherit default resources](https://kiro.dev/docs/custom-agents/configuration-reference/) alongside their own list unless `chat.disableInheritingDefaultResources` is on. Setting `x-kiro.resources` on an agent does not change this. If you turn that setting on, add `file://AGENTS.md` to each agent's `x-kiro.resources`. `agnostic-ai lint` warns (LINT029) when an agent with resources leaves it out. See [target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). - **Memory**: with `builtins: [memory]`, `.kiro/steering/kiro-memory-index.md` includes `.agnostic-ai/memory/MEMORY.md` and `.agnostic-ai/local/memory/MEMORY.md` with `#[[file:...]]`, so Kiro loads both [shared memory](https://agnostic-ai.org/docs/memory/) lists every session. No other tool gets the file, and `sync --global` leaves it out. Kiro resolves each path from the workspace root and shows a marker for a list that does not exist yet. It applies its file read permissions and ignore rules to a reference, so a list they block does not load. With `memory.personal: repo`, the file includes only the project list. The shared policy tells Kiro to find personal memory with `agnostic-ai memory path`. - **Skills**: [native](https://kiro.dev/docs/skills/) at `.kiro/skills//SKILL.md`, read by the skill picker. The file (`name` and `description` frontmatter) is identical to the shared `.agents/skills/` one. Bundled `scripts/`, `references/`, and `assets/` are copied with it. - **Agents**: [native custom agents](https://kiro.dev/docs/custom-agents/) in YAML-frontmatter Markdown, read by the agent picker. `description` (default: the agent name) and `model` are copied as written. The spec name stays the file name. `x-kiro.name` sets Kiro's optional display name and survives import. - **Agent MCP servers**: each server an agent names in `mcpServers` is written in full under the agent's `mcpServers`, the only form Kiro takes. An [agent-only server](https://agnostic-ai.org/docs/spec-format/mcps/#agent-only) stays out of `.kiro/settings/mcp.json`. `x-kiro.mcpServers` replaces the list. ## Commands Kiro CLI 2.27.0 and newer shows [workspace prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) as `/name` commands in V3. Sync writes the command body to `.kiro/prompts/.md`, with a generated-file header and no frontmatter. Kiro expands `$ARGUMENTS`, `${1}` through `${10}`, and `${@}`. Sync keeps these tokens as they are. Use a `::target kiro` fence for Kiro-specific templates. Bare `$1` and `$ARGUMENTS[0]` still get the unsupported-syntax checks. `outputs.kiro.commands-dir` changes the prompt folder. Otherwise, `outputs.kiro.dir` puts prompts under `/prompts`. Import reads the same folder into `sources.commands`. Kiro also has a global prompt folder. `sync --global` does not write commands. ### Unmanaged AGENTS.md Listing `AGENTS.md` under `sync.unmanaged` keeps every steering file. Check [the effect on other targets sharing that file](https://agnostic-ai.org/docs/target-behavior/#inlined-rules). ### Old flattened steering files Older versions flattened skills into `.kiro/steering/skill-.md` and agents into `.kiro/steering/agent-.md`, which never reached the pickers. `sync` removes them. Kiro's [agent schema](https://kiro.dev/docs/custom-agents/configuration-reference/) also carries `tools`, `mcpServers`, `permissions`, `hooks`, `keyboardShortcut` (CLI 2.x only), `welcomeMessage`, `excludedTools`, `includeMcpJson`, and `includePowers`. `tools` is translated from the portable `can` or `tools` field. Kiro's `mcpServers` holds inline definitions, not names, so set `x-kiro.mcpServers`. Set the rest, and any other key, under `x-kiro`. Kiro groups agent tools into `read`, `write`, `shell`, `web`, and `subagent`. The [configuration reference](https://kiro.dev/docs/custom-agents/configuration-reference/) and [tools page](https://kiro.dev/docs/tools/) list the same categories. It also accepts `@server_name`, `@server_name/tool_name`, `@mcp`, `@builtin`, and `*`. Built-in tools such as `knowledge` use their exact tool IDs. Sync translates four of the categories, plus named MCP servers and tools: | Spec `can` values | Spec `tools` values | Kiro category | | --- | --- | --- | | `read` | `Read`, `Grep`, `Glob` | `read` | | `write`, `edit`, `delete` | `Write`, `Edit` | `write` | | `shell` | `Bash` | `shell` | | `web` | `WebFetch`, `WebSearch` | `web` | | `mcp:` | `mcp__` | `@` | | `mcp:/` | `mcp____` | `@/` | Duplicates are merged. A Kiro category covers more than one tool, so access widens: `write` also covers `delete_file`, and `web` covers fetch and search. Sync prints a note naming the extra access, such as `kiro: .agnostic-ai/agents/writer.md: edit becomes Kiro's write category, which also allows delete_file`. `on-unsupported: error` fails when access widens, including for a Claude Code alias such as `Edit`. Scoped values, such as `shell(git diff *)` and `read(src/**)`, are dropped with a sync note. The rest are still written. `x-kiro.tools` takes Kiro's own names and always wins. **Hooks** are [native](https://kiro.dev/docs/hooks/): one JSON file per spec, `{"version": "v1", "hooks": [{name, trigger, matcher, action, timeout, enabled, description}]}`. - Portable `on: before-tool` maps to `PreToolUse`, with anchored lists of documented built-in IDs for `shell`, `read`, `web`, and `edit`, and `@/*` for `mcp:`. Built-in kinds exclude MCP tools and need an updated mapping for new IDs. The tool wildcard keeps server names such as `mcp` distinct from source selectors. Other portable events and `decision: stdout` have no Kiro mapping. See [portable hooks](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). - `event` becomes `trigger`, as written. `validate` flags `AgentSpawn` and `agentSpawn`, Kiro V3's compatibility aliases, and suggests `SessionStart`. - Each `command` (string or list) becomes an `action: {"type": "command", "command": ...}` entry in the same file. The `name` gets a `-2`, `-3`, ... suffix to stay unique. - `disabled: true` writes `"enabled": false`. `description` is written to the file as documentation only. - `timeout: 0` disables the timeout; omitting it keeps Kiro's 60-second default. - `x-kiro` keys are copied to each entry. That is the only way to set `confirm` (ask before a Stop command hook runs, with `question`, `options` of `id`/`label`/`run`, and optional `confirmCommand`). - `x-kiro.action` takes `{type: agent, prompt: ...}` or `{type: command, command: ...}`. A valid one needs no portable `command` and replaces the whole list with one action. An invalid one fails sync. - `.agnostic-ai/scripts/` references are copied to `.kiro/scripts/`, outside `.kiro/hooks/` where Kiro reads hook files, and the command is rewritten. See [shared hook scripts](https://agnostic-ai.org/docs/spec-format/hook-recipes/#shared-hook-scripts). - [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) runs a `UserPromptSubmit` or `Stop` hook, or a tool hook with `--payload`, on an assumed `sh -c`, before a session does. Use your home hook specs in every workspace with [Kiro CLI V3](https://kiro.dev/docs/hooks/#file-naming-and-location): ```bash agnostic-ai sync --global --only kiro --dry-run agnostic-ai sync --global --only kiro agnostic-ai sync --global --only kiro --check ``` Sync writes `~/.kiro/hooks/.json` and copies shared scripts to `~/.kiro/scripts/`. Set `KIRO_HOME` to a valid absolute profile directory to change where agnostic-ai writes these folders and Kiro's other global files. Like [Kiro CLI](https://kiro.dev/docs/reference/settings/#environment-variables), sync uses `~/.kiro` when the value is relative, a filesystem root, your home directory, or one of its ancestors. Global hooks and workspace hooks both run. Sync removes only recorded files after their specs are removed. A conflicting or edited file stops sync. `agnostic-ai import --global kiro` imports hook files that one spec can reproduce at the same filename. Files with mixed triggers, names that differ from their filename, or other content the spec cannot preserve are skipped with a warning. Existing specs stay. After importing, run the sync and check commands above. **MCP** servers go to `.kiro/settings/mcp.json` under `mcpServers`, Kiro's [workspace-level config](https://kiro.dev/docs/mcp/configuration/). Local servers have `command` plus optional `args` and `env`. Remote ones have `url` plus optional `headers` and `env`. Kiro expands a `${NAME}` reference only after you approve the variable under **Mcp Approved Env Vars** in its settings. See [environment references](https://agnostic-ai.org/docs/spec-format/mcps/#environment-references). `disabled` is copied as written (default `false`). Kiro also accepts `autoApprove` (tools approved without prompting, `"*"` for all) and `disabledTools` (tools hidden from the agent). A remote server can add `oauth` (`{clientId, clientSecret, redirectUri, clientMetadataUrl, oauthScopes}`) and a top-level `oauthScopes` fallback; `oauth.oauthScopes` wins. An empty `oauthScopes: []` is written as is, which is Kiro's documented fix for scope errors. Kiro's `oauth` differs from Claude Code's, so each tool maps only its own sub-keys. See [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target). **Ignore** specs are joined into the project `.kiroignore` in gitignore syntax ([Kiro ignore](https://kiro.dev/docs/kiroignore/)); override with `outputs.kiro.ignore-file`. The IDE honors the file only when it is listed in `kiroAgent.agentIgnoreFiles` (Kiro suggests `[".gitignore", ".kiroignore"]`). CLI V3 honors workspace `.kiroignore`, `~/.kiro/settings/kiroignore`, and Git's global ignore file (`core.excludesfile`, only in Git repositories). Matching files are blocked from direct reads and hidden from content and filename search. ## Config keys | Key | Default | | --- | --- | | `outputs.kiro.rules-dir` | `.kiro/steering` | | `outputs.kiro.agents-dir` | `.kiro/agents` | | `outputs.kiro.skills-dir` | `.kiro/skills` | | `outputs.kiro.commands-dir` | `.kiro/prompts` | | `outputs.kiro.hooks-dir` | `.kiro/hooks` | | `outputs.kiro.mcp-file` | `.kiro/settings/mcp.json` | | `outputs.kiro.ignore-file` | `.kiroignore` | ## Import `agnostic-ai import kiro` reads the native agent and skill folders first. Then it reads rules and old flattened agents and skills from `.kiro/steering/` (the filename prefix picks the kind): | Source | Becomes | |--------|---------| | `.kiro/prompts/.md` (or the configured command directory) | `/.md` | | `.kiro/agents/.md` (native agent profile) | `/.md` | | `.kiro/skills//SKILL.md` (native skill folder) | `//SKILL.md`, bundled files next to the skill included | | `.kiro/steering/.md` (`inclusion: always`) | `/.md` (project-wide rule) | | `.kiro/steering/.md` (`inclusion: fileMatch` + `fileMatchPattern`) | `/.md` with `globs: ` | | `.kiro/steering/.md` (`inclusion: manual` or `auto`) | skipped with a note. The file stays hand-written, since a rule has no on-demand mode | | `.kiro/steering/agent-.md` (legacy, pre-native sync) | `/.md`, body only | | `.kiro/steering/skill-.md` (legacy, pre-native sync) | `//SKILL.md`, body only | | `.kiro/hooks/.json` (`hooks[]`) | one hook spec per group of entries that differ only in `action.command` | | `.kiro/settings/mcp.json` (`mcpServers.`) | `/.yaml` | | `AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md` | Native files win over old steering files with the same name. Hook import reads every Kiro field: - `trigger` becomes `event`; `matcher`, `description`, and `timeout` (including `timeout: 0`) carry over; `enabled: false` becomes `disabled: true`. - `action.type: "agent"` goes under `x-kiro.action`, not the portable prompt handler, which [spec-format.md](https://agnostic-ai.org/docs/spec-format/hooks/) limits to Claude Code, Cursor, and Copilot. - `confirm` and unknown keys go under `x-kiro`, so new Kiro fields are kept. - Several `trigger` values split into one spec each. - Entries that differ only in `action.command` become one `command:` list, without the `-2`/`-3` suffixes. - A readable name ("Lint on save") becomes the file name in slug form and stays unchanged in `name:`. - An action with no content (`type: "command"` with no `command`, `type: "agent"` with no `prompt`) is skipped with a warning. The imported spec loses some data, but the Kiro files sync writes stay the same: - A rule's folder scope becomes an equivalent `globs:`. - An old flattened agent or skill keeps only its body. That form never had a description, model, or bundled files. - An explicit `enabled: true` leaves no key, since it is the default. - Two hook files with the same `name` keep both hooks. The second gets a generated name, because spec names are unique file names. - {{filePath}} stays as written. Kiro documents it as new in 3.0, and it means nothing elsewhere. ## Global voice language `agnostic-ai sync --global --only kiro` writes home `voice.language` to the literal `"voice.language"` key in `~/.kiro/settings/cli.json`, or `settings/cli.json` under `KIRO_HOME`. Use `auto` to detect the transcription language. Kiro CLI has no on/off or capture mode key, so sync prints a note for `voice.enabled` and `voice.mode`. Other voice settings, including `voice.autoSubmit`, stay as written. Removing the portable language restores its previous value; a value you changed by hand stays yours. See [voice dictation](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation). ## Protected paths Not enforced. Kiro takes no settings specs in project sync, so it reports a spec with a `protected` block as unsupported. Global voice language uses the separate home settings mapping above. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Crush (`crush`) Charm [Crush](https://github.com/charmbracelet/crush) reads `AGENTS.md`, `.agents/skills/`, `crush.json`, and `.crushignore`. Set `outputs.crush.agents: skill` to write agents as on-demand skills; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills). ## First rule Select `crush` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Crush has no native custom-agent profiles. For existing files, check [what `import crush` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Launch `crush` from the project and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body + inlined rules (shared path) .agents/skills//SKILL.md # one folder per skill (shared tree) crush.json # when MCP or hook entries exist (merged with existing user config) .crushignore # project-root ignore patterns ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: Crush has no per-rule directory, so rule bodies are included in `AGENTS.md`. - **Skills**: `.agents/skills/` is the first project path Crush scans. While another target writes that folder, the files are identical to those the other `.agents/skills/` tools get, so it is written once. `x-crush.user-invocable: true` also adds a skill to the command palette (ctrl+p). - `disable-model-invocation: true` is written with `user-invocable: true`. Crush then leaves the skill out of the model's list and keeps it in the command palette ([Crush README](https://github.com/charmbracelet/crush/blob/v0.98.1/README.md), since v0.71.0). So each command reaches Crush as a skill the user runs by name ([tools without commands](https://agnostic-ai.org/docs/spec-format/commands/#tools-without-commands)). When another target, including Goose, also writes the skills folder, the files must match, so both fields are dropped with a note and commands are skipped; set `outputs.crush.skills-dir: .crush/skills` to keep them. An `x-crush` value for either field wins. Import reads the pair back as `disable-model-invocation: true`, so after import every selected tool treats that skill as user-only. A native file that keeps `user-invocable` but drops `disable-model-invocation` imports as `x-crush.disable-model-invocation: false`, so Crush's model can start the skill again while other tools keep it user-only. - **Agents**: Crush has no native agent profiles. By default, agents are skipped with a warning; `outputs.crush.agents: skill` converts them to skills. - **`crush.json`**: sync keeps your `models`, `providers`, `lsp`, and `options` keys. Crush now prefers `crushrc`, a Bash script it runs on startup, and adds new options only there. JSON is deprecated but still works, so sync writes `crush.json` and never `crushrc`. An MCP field that exists only in `crushrc` cannot be set here. - **Ignore**: `.crushignore` uses gitignore syntax, supported since [Crush v0.94.1](https://raw.githubusercontent.com/charmbracelet/crush/v0.94.1/README.md). The usual protection for hand-written files applies. ### A hand-written crushrc too Crush merges every config file: `./.crushrc`, then `./crushrc`, then `$XDG_CONFIG_HOME/crush/crushrc`. It also merges the legacy `crush.json` and `.crush.json` (project over global, `crushrc` over JSON in the same directory). A directory with both logs a warning on every launch. ### MCP Servers merge into the `mcp` key of `crush.json`: - stdio: `{type: stdio, command, args, env}` - HTTP: `{type: http, url, headers, oauth, oauth_client_id, oauth_client_secret, oauth_callback_port}` - SSE: `{type: sse, url, headers, oauth, ...}` The oauth fields are optional and need Crush v0.87.0. `sse` stays `sse`, because Crush uses a different transport for it than for `http`. A spec's `remote` type defaults to `http`. Every transport also accepts these fields from [Crush's `schema.json`](https://raw.githubusercontent.com/charmbracelet/crush/v0.98.1/schema.json): - `disabled` is written as is. - `sessionless` marks a server that sends no `Mcp-Session-Id`, so Crush skips the subscription stream (Crush v0.91.2). When unset, Crush detects known cases such as GitHub MCP. - `enabled_tools` and `disabled_tools` choose which tools the agent gets. - `channel_enabled` opts into incoming MCP messages (Crush v0.97.0). An explicit `false` stays false. - `channel_reply` sets automatic replies: `user` routes direct messages, `group` routes group messages. Each route requires `tool` and `target_param`; optional `target_meta` chooses the incoming message attribute with the reply target. It defaults to `sender` for direct messages and `group` for group messages. `message_param` names the reply text argument (default `message`), and `suppress_tools` lists tools that prevent a duplicate automatic reply. Set the channel fields at the top level or under `x-crush`, which wins. Import keeps them at the top level. Sync checks channel types, required route fields and unknown nested keys before writing `crush.json`. Other `x-crush` MCP keys are ignored because Crush rejects unknown MCP fields. Skills still accept `x-crush` frontmatter. ```yaml name: chat command: channel-server x-crush: channel_enabled: true channel_reply: user: tool: send_message_to_user target_param: user_id group: tool: send_message_to_group target_param: group_id target_meta: room message_param: message suppress_tools: [send] ``` ### Hooks Hooks merge into `crush.json` under `hooks`, beside `mcp`. - Crush v0.99.0 supports `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionResult`, `SessionStart`, `SessionEnd`, `TurnStart`, `TurnEnd`, `StopFailure`, `Interrupt`, `PreCompact`, and `PostCompact` ([Crush hooks](https://github.com/charmbracelet/crush/blob/v0.99.0/docs/hooks/README.md)). Unknown events get a sync note. - Sync accepts any case or snake_case spelling and writes the canonical event name, such as `session_start` to `SessionStart`. - Each hook is one flat array item (`{"name": ..., "matcher": ..., "command": ..., "timeout": ...}`), not the Claude-style `{"matcher": ..., "hooks": [...]}` group. `command` is required. `timeout` is in seconds, default 30. - Crush runs a command as a script only when it starts with `./`, `../`, or `/`. A hook command that starts with a `.agnostic-ai/scripts/` script, which sync copies, becomes `./.crush/hooks/`. Other commands, such as `bin/guard` or `.crush/hooks/guard`, stay as written. Import keeps every command as written. - Matchers are accepted only for `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, and `PermissionResult`. Sync rejects a matcher on another event because Crush treats it as a configuration error. - `SessionStart` and permission events are observe-only. `SessionEnd` shares a two second budget across its hooks. Native event names do not imply the same control behavior as another tool. - Tool names are lowercase (`bash`, `edit`, `write`, `mcp__`, for example `^bash$`). A Claude-style matcher (`Bash`, `Edit`) matches nothing, so `sync` prints a note that the field has no effect. - [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) tests `PreToolUse` before a session, using Crush's embedded shell, event data, and timeout. Other Crush events are not supported by that diagnostic. ## Config keys | Key | Default | Notes | | --- | --- | --- | | `outputs.crush.skills-dir` | `.agents/skills` | | | `outputs.crush.agents` | unset (agents are dropped) | `skill` writes each agent as an on-demand skill. Sync keeps it off with a note while another tool with subagents writes the same skills folder; see [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills) | | `outputs.crush.mcp-file` | `crush.json` | also the hooks file | | `outputs.crush.ignore-file` | `.crushignore` | | ## Import `agnostic-ai import crush` reads: | Source | Becomes | |--------|---------| | `AGENTS.md` embedded `## Rules` block (`### ` children) | `/.md` per rule | | `.agents/skills//SKILL.md` (+ bundled files) | `//SKILL.md` (folder copied unchanged) | | `crush.json` (`mcp.`, `type: stdio` / `type: http` / `type: sse`) | `/.yaml` | | `crush.json` `hooks.` | hook specs for each supported event; a unique safe name becomes the filename. Handlers with distinct labels or timeouts use separate spec names and `x-crush.name`, so native identities survive a round trip | | `.crushignore` | an ignore spec | | `AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md` | Import leaves commands emitted by personal hook specs in the personal layer for every supported lifecycle event. Crush has no documented directory scope. Sync skips scoped rules, and import cannot recover scope from the merged instructions. See [scoped context](https://agnostic-ai.org/docs/scoped-context/). ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Trae (`trae`) ByteDance [Trae](https://docs.trae.ai/ide/rules) reads `.trae/rules/`, the root `AGENTS.md`, and project subagents in `.trae/agents/`. ## First rule Install the client using the [official setup guide](https://www.trae.ai), then open your project in it. Select `trae` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.trae/rules/conventional-commits.md`. Project hooks and subagent directories require separate switches in Trae settings. Syncing their files does not enable those features. For existing files, check [what `import trae` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `.trae/rules/conventional-commits.md` for the commit-prefix rule. Open the project in Trae. Confirm the rule is active in the Rules panel, then ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # pointer body, plus the rules block when another tool that inlines rules shares it .trae/rules/.md # one per rule .trae/agents/.md # one per agent (project subagent) .trae/skills//SKILL.md # one folder per skill .trae/commands/.md # one per command .trae/hooks.json # when hook entries exist .trae/.ignore # when ignore entries exist .trae/mcp.json # MCP server registry ``` ## Native mappings ### Field mappings, discovery, and compatibility Trae reads `AGENTS.md` only when **Include AGENTS.md in the context** is on (Settings > Rules & Memories). So every rule keeps its `.trae/rules/` file. With that switch on and codex or another tool that includes rules in instruction files enabled, always-on rules load twice. See [target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). ### Rules Sync always writes `description`, `globs`, and `alwaysApply` (Cursor's `.mdc` fields for when a rule applies), because Trae documents no default for a file without them. - `alwaysApply` is `true`, or `false` when the spec sets `globs` other than a catch-all such as `**/*`. A `true` rule omits `globs`. - `alwaysApply: false` with no `globs` uses the Claude `paths` list, joined with commas. - `x-trae.scene: git_message` marks a rule for AI-written commit messages. ### Agents ([subagents docs](https://docs.trae.ai/ide/subagents)): frontmatter carries `name` and `description` (required), plus optional `model`, `tools`, `disallowedTools`, and `mcpServers`. The body is the system prompt. - `tools` is written as is, as a string joined with commas, because Trae uses Claude-style names (`Bash`, `Edit`, `Glob`, `Grep`, `Read`, `Write`, `WebFetch`, `WebSearch`, plus `Skill`, `LSP`, `TodoWrite`, and `mcp____`). - Omit `tools` to load every available tool. An explicitly empty tool list (`can: []`, `tools: []`, `tools: ""`, or `x-trae.tools`) writes `tools: ""`, which disables all tools. Import preserves this restriction through later syncs. - Trae accepts only its built-in model IDs (`gpt-5.4`, `minimax-m3`, ...). A generic `model` drops with a sync note. Use `model: {trae: }` or `x-trae.model`. - `disallowedTools` and `mcpServers` come from `x-trae`. `x-trae.disallowedTools` is a comma-separated list of blocked tools, and it wins over `tools`. - Names start with an ASCII letter, end with a letter or digit, use only letters, digits, or hyphens, and run at most 50 characters. Sync rejects any other name. - Subagents need Settings > Beta > Subagents > Enable Subagents Directory. Trae does not say whether it is on by default. ### Skills ([skills docs](https://docs.trae.ai/ide/skills)): `.trae/skills//SKILL.md` with `name` and `description`. Files next to the skill (`examples/`, `templates/`, `resources/`) are copied as they are. A flat file under `.trae/rules/` never loads as a skill. ### Commands ([slash-commands docs](https://docs.trae.ai/ide/slash-commands)): only `name` and `description` frontmatter (the documented fields), with the body as the prompt. Trae reads commands up to 3 levels deep, and sync writes them flat. ### MCP servers ([MCP docs](https://docs.trae.ai/ide/add-mcp-servers)): a root `mcpServers` map. Stdio entries carry `command` (required), `args`, and `env`. HTTP entries carry `url` (required) and `headers`. Trae works out the `type` from `command` or `url`, so none is written. - A stdio `command` with spaces fails to parse in Trae. - Trae has no per-server `disabled` key, only a project toggle (Settings > MCP). `disabled: true` is dropped with a sync note. ### Hooks `.trae/hooks.json`, the project tier of Trae's [hook configuration](https://docs.trae.ai/ide/hook-configuration-reference). [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) tests them before a session, using Trae's event data, shell, and timeout. - A `version` field (always 1) wraps the `hooks` map. Each event holds `{matcher, hooks: [{type, command, timeout}]}` groups. Claude Code, Codex, OpenHands, and Qoder use the same shape, so one hook spec feeds all five. - Six [events](https://docs.trae.ai/ide/automate-actions-with-hooks): `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `Notification`. Each entry takes `type` (only `command`), `command`, and `timeout` (seconds, default 30). - `loop_limit` caps how often a `Stop` hook can block the agent (default 5). - **Hook `tool_name` values differ from subagent `tools`**: `Read`, `Write`, `Edit`, `Glob`, `Grep`, `LS`, `RunCommand`, `WebSearch`, `WebFetch`, `AskUserQuestion`, `Skill`, and `mcp____`. There is no `TodoWrite`. The terminal tool is `RunCommand`, so `matcher: Bash` is written as is, raises a sync note, and matches nothing. - `matcher` applies to `PreToolUse`, `PostToolUse`, and `Notification` only. On `Notification` it selects a type (`idle_prompt`, `permission_prompt`, ...). - Project hooks do nothing until you enable them under Settings > Hooks and accept the security warning. - Trae also runs Claude Code's hooks from `.claude/settings.json` and `.claude/settings.local.json` once you turn on **Import Hook configuration in CLAUDE** (off by default, same warning). Syncing `claude` and `trae` then runs every hook twice. ### Ignore ([ignore docs](https://docs.trae.ai/ide/ignore-files)): the file Trae's Settings > Indexing & Docs creates. It adds to `.gitignore`, which Trae already honors, and keeps paths out of indexing and `@Folder` context. Specs are joined. Override with `outputs.trae.ignore-file`. Run a Build under Settings > Indexing & Docs after a sync, because the change applies only after re-indexing. ## Shared memory With `builtins: [memory]`, sync adds the native `memory-trae-session-start` hook for `SessionStart` to `.trae/hooks.json`. Once you enable project hooks in Settings > Hooks, it runs `agnostic-ai project -- hook memory --target trae` before the first message and adds up to 6,000 bytes from the personal and project memory indexes. Trae's own Memories stay available. Trae sets `TRAE_PROJECT_DIR` to the workspace used by the hook, so the command finds that project's indexes even when its shell starts elsewhere. The command uses the project's installed agnostic-ai package when one is declared; install or upgrade that package before starting Trae. If hooks are disabled, the shared memory rule still tells Trae to read the indexes. ### Agents from older versions For every current agent, sync removes a generated copy at the old `.trae/rules/agent-.md` path. ## Config keys | Key | Default | | --- | --- | | `outputs.trae.rules-dir` | `.trae/rules` | | `outputs.trae.agents-dir` | `.trae/agents` | | `outputs.trae.skills-dir` | `.trae/skills` | | `outputs.trae.commands-dir` | `.trae/commands` | | `outputs.trae.hooks-file` | `.trae/hooks.json` | | `outputs.trae.ignore-file` | `.trae/.ignore` | | `outputs.trae.mcp-file` | `.trae/mcp.json` | ## Import `agnostic-ai import trae` reads these back: | Source | Becomes | |--------|---------| | `.trae/rules/*.md` | rules, agents, and skills by [filename prefix](https://agnostic-ai.org/docs/cli-reference/start/#filename-prefix-reclassification) | | `.trae/agents/.md` | `/.md`, copied, minus the generated header | | `.trae/skills//SKILL.md` and `.agents/skills//SKILL.md` (+ bundled files) | `//SKILL.md` (folder copied as is) | | `.trae/commands/.md` | `/.md` | | `.trae/hooks.json` | one hook spec per matcher group, carrying `loop_limit` | | `.trae/mcp.json` (`mcpServers.`) | `/.yaml`, a `url`-only entry becomes `type: http` | | `.trae/.ignore` | an ignore spec | Import reads `.trae/skills/` first, then `.agents/skills/`. When both hold a skill with the same folder name, Trae's own copy wins. Shared skills keep the same checks for existing specs and generated assets. Sync still writes `.trae/skills/` by default. Hook commands that share an event and matcher become one spec with a `command:` list, as sync merges them into one group. Import skips `version` (always 1), a `loop_limit` of 0 (Trae treats it as unset), and the global `~/.trae/hooks.json`. ## Protected paths Not enforced. Trae takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Qoder (`qoder`) agnostic-ai writes Alibaba [Qoder](https://docs.qoder.com/user-guide/rules) rules, agents, skills, and commands under `.qoder/`, and merges MCP, hooks, and settings into `.qoder/settings.json`. ## First rule Install the client using the [official setup guide](https://qoder.com), then open your project in it. Select `qoder` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `.qoder/rules/conventional-commits.md`, or shared `AGENTS.md` when another selected target already puts the rule there. When another target includes the rule in `AGENTS.md`, Qoder uses that copy. Keep `AGENTS.md` in the CLI context file list. For existing files, check [what `import qoder` reads](#import) before syncing. With `handoff` or `handoff-hook`, linked worktrees get shared handoff write access through `permissions.additionalDirectories` in the ignored `.qoder/settings.json`. See [shared handoff access](https://agnostic-ai.org/docs/handoff/#write-from-a-linked-worktree) for preserved configuration and fallback behavior. ## Verify Run `agnostic-ai sync --check` and find the commit-prefix rule in `.qoder/rules/conventional-commits.md` or the shared `AGENTS.md`. Open the project in Qoder. Confirm the rule is active, then ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body, plus the rules block when another tool adds it .qoder/rules/.md # one per rule that AGENTS.md does not carry, or that is not always-on .qoder/agents/.md # one per agent .qoder/skills//SKILL.md # one folder per skill, plus bundled assets .qoder/commands/.md # one per command .qoder/settings.json # MCP, hooks, and settings (merged; other keys are kept) ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Rules Qoder reads `.qoder/rules/` and the root `AGENTS.md`. Per-rule files win a conflict, so rules go there instead of including them in that file. When Codex or another tool that copies rules into `AGENTS.md` adds the `## Rules` block, an always-on rule gets no file and loads once. A rule whose text differs for Qoder keeps its file. The Qoder CLI reads `AGENTS.md` unless `context.fileName` names other files. Keep it listed there, or the CLI misses the rules that have no file. See [Rule activation](#rule-activation) and [target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). ### Agents [Qoder Subagents](https://docs.qoder.com/extensions/subagent) reads `.qoder/agents/.md`. `name` and `description` are required. `model`, `tools`, `color`, `skills`, `mcpServers`, `effort`, `permissionMode`, `memory`, `hooks`, `maxTurns`, and `background` are optional. In `mcpServers`, an [agent-only server](https://agnostic-ai.org/docs/spec-format/mcps/#agent-only) is written in full and stays out of `.qoder/settings.json`. `effort` takes a single value or a per-tool map, and an integer budget stays a number. See [per-target `model` and `effort`](https://agnostic-ai.org/docs/spec-format/agents/#per-target-model-and-effort) and [agent policy support by target](https://agnostic-ai.org/docs/spec-format/agents/#agent-policy-support-by-target). - `color` is one of `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan` ([CLI field reference](https://docs.qoder.com/cli/subagent)). - `tools` renders as a comma-separated string (`tools: Read, Grep, Bash`), which `import qoder` splits back. Qoder uses Claude-style names (`Bash`, `Edit`, `Write`, `Glob`, `Grep`, `Read`, `WebFetch`, `WebSearch`), so the portable list is copied unchanged. - `skills` and `mcpServers` come from `x-qoder`. `memory` is copied unchanged. See [Subagent memory](#subagent-memory). - Sync sets `name`, `description`, `model`, `tools`, `skills`, and `mcpServers`. Any other `x-qoder` key is copied as written. ### Skills Qoder's own `.qoder/skills//SKILL.md` ([Qoder Skills](https://docs.qoder.com/extensions/skills)), with plain `name` and `description` frontmatter. Bundled files (scripts, references, templates) are copied unchanged next to `SKILL.md`. Project sync writes this native folder; `sync --global` writes personal skills under `~/.qoder/skills/`. [Qoder CLI 1.1.11](https://docs.qoder.com/release-notes/qoder-cli#1111-2026-08-01) (August 1, 2026) enabled `.agents/skills/` compatibility by default. The IDE skills guide documents the native `.qoder/skills/` path. Sync keeps that native default and does not combine it with the shared skills folder. ### Commands `.qoder/commands/.md`, the project path for the [CLI](https://docs.qoder.com/cli/commands) and the [IDE](https://docs.qoder.com/user-guide/commands). Sync always writes `description` (default: the command's name) and never `name`, because the file path sets it. On a name clash, the CLI prefers your personal command (`~/.qoder/commands/`), and the IDE lists both with a scope label. Sync cannot see your personal commands, so it cannot warn. ### MCP `mcpServers` is merged into `.qoder/settings.json` (stdio: `command`/`args`/`env`/`cwd`, no `type`; remote: `type` plus `url`/`headers`). Other keys such as `mcp.enableAllProjectMcpServers`, permissions, and custom models are kept. - Entries accept these [optional fields](https://docs.qoder.com/cli/mcp-reference): `timeout` (milliseconds), `description`, `trust` (skip tool-call confirmation), `includeTools`, `excludeTools`, `alwaysAllow`, `disabled` (keep the config, turn the server off), and an `oauth` object, written as you declare it. See [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target). - A `type: ws` spec gets only a sync note. Qoder's ws transport takes a TCP host and port that a spec cannot express. - The file may be JSONC ([settings](https://docs.qoder.com/cli/settings)). `sync` keeps the keys but drops comments and trailing commas, and warns. ### Settings The shared default model becomes `model.name`. `permissions.allow`, `permissions.deny`, and `permissions.ask` are copied directly. An `x-qoder` settings block is merged in too. Voice has no documented native key. - With `memory.personal: repo`, sync adds the personal store to `permissions.additionalDirectories`, so Qoder approves saves there. Your own entries stay. See [repo mode](https://agnostic-ai.org/docs/memory/#one-store-per-repository). ### Hooks Hooks are merged under `hooks` in the same file ([Qoder hooks](https://docs.qoder.com/cli/hooks)), in the nested form that Claude Code, Codex, and OpenHands share. With `builtins: [memory]`, a `SessionStart` hook adds the [shared memory](https://agnostic-ai.org/docs/memory/) index to the session. Its reply is `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": ...}}`, the form Qoder documents. - 27 PascalCase events ([hooks reference](https://docs.qoder.com/cli/hooks-reference)): `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied`, `Stop`, `StopFailure`, `SubagentStart`, `SubagentStop`, `PreCompact`, `PostCompact`, `Notification`, `InstructionsLoaded`, `ConfigChange`, `CwdChanged`, `FileChanged`, `WorktreeCreate`, `WorktreeRemove`, `Elicitation`, `ElicitationResult`, `TaskCreated`, `TaskCompleted`, `TeammateIdle`, `Setup`. `event:` is copied as written. Listed events are sorted in Qoder's order, ahead of unlisted ones. - `PreToolUse`/`PostToolUse` matchers use Claude Code's tool names (`Bash`, `Write`, `Edit`, `Read`, `Glob`, `Grep`, `mcp__server__tool`), so matchers written for Claude Code work unchanged. - Command entries have `command`, `type: command`, optional `args`, `timeout` (seconds, default 600), `statusMessage`, `async`, `asyncRewake`, `shell` (`bash` or `powershell`), `if` (a permission rule filter, for example `Bash(git *)`), and `once`. - `args` switches to the form without a shell: `command` is one program and each `args` element is one literal argument. Qoder then ignores `shell`. Sync still writes both and adds a note. - `once` has no effect, and `sync` says so. Qoder honors it only for session-scoped hooks ([Hooks](https://docs.qoder.com/cli/hooks), [Subagent](https://docs.qoder.com/cli/subagent)), and portable hooks go in settings. - HTTP handlers have `url`, optional `headers`, and `allowedEnvVars`. Prompt handlers have `prompt` and optional `model`. Both keep filters, timeouts in seconds, and matcher groups. - Other handler types get a sync note. Sync writes only one `env` entry, `AGNOSTIC_AI_TARGET`, and no `rewakeMessage`, `rewakeSummary`, or agent handlers. ### Unmanaged AGENTS.md Listing `AGENTS.md` under `sync.unmanaged` makes every rule keep its file. Check [the effect on other targets sharing that file](https://agnostic-ai.org/docs/target-behavior/#inlined-rules). ### Leftover root .mcp.json Older versions wrote MCP servers to the root `.mcp.json` shared with Claude Code. The two tools' per-server fields differed, so Qoder moved to `.qoder/settings.json`, which Claude Code never reads. A leftover `.mcp.json` still loads and wins over `.qoder/settings.json` for a server with the same name. Sync cannot remove it, because it has no generated-file header and may belong to Claude Code. Delete it by hand in a Qoder-only project. ## Config keys | Key | Default | | --- | --- | | `outputs.qoder.rules-dir` | `.qoder/rules` | | `outputs.qoder.agents-dir` | `.qoder/agents` | | `outputs.qoder.skills-dir` | `.qoder/skills` | | `outputs.qoder.commands-dir` | `.qoder/commands` | | `outputs.qoder.mcp-file` | `.qoder/settings.json` (also the hooks path) | ## Rule activation Qoder rules keep `description`, `alwaysApply`, `trigger`, `glob`, and `paths` frontmatter. Native `trigger`, `glob`, and `paths` import under `x-qoder`. `description` and `alwaysApply` use the rule's own fields. A single selector stays single and a list stays a list. Native fields under `x-qoder` override the top-level fields. [Qoder's rule activation modes](https://docs.qoder.com/cli/memory#rule-activation-methods): | Mode | Frontmatter | | --- | --- | | Always active | No activation fields, `trigger: always_on`, or `alwaysApply: true` | | Manual | `trigger: manual` or `alwaysApply: false` | | Model-selected | `trigger: model_decision` plus a non-empty `description` | | File matching | `trigger: glob` plus `glob`, or `paths` | `trigger` wins over `alwaysApply`. A scoped rule gets `paths` covering the scope folder and the file patterns together, with no other activation fields. Validation rejects selectors that sync cannot keep. For a manual release rule: ```markdown --- name: release x-qoder: trigger: manual --- Run release steps only on request. ``` ## Subagent memory The portable `memory` field is copied unchanged to `.qoder/agents/.md`. Qoder's [subagent reference](https://docs.qoder.com/cli/subagent) accepts the same scopes: `user`, `project`, `local`. Only the Markdown file has it. The `--agents` JSON format has no `memory`. It works only when top-level `autoMemoryEnabled` is on in [settings](https://docs.qoder.com/cli/settings-reference) (default `false`). Qoder documents this, but it is not tested at runtime. Only Claude Code is confirmed to act on `memory`. ## Auto memory Qoder's own memory store is separate from the field above ([Qoder memory](https://docs.qoder.com/cli/memory)). It lives at `~/.qoder/projects//memory/` (project) and `~/.qoder/memory/` (user). Each is a `MEMORY.md` index plus one file per topic. `/memory` shows them. `/memory manage` views, edits, or deletes a topic. A session loads the first 200 lines or about 25KB of each active `MEMORY.md`. Auto memory is off by default; see [each tool's own memory](https://agnostic-ai.org/docs/memory/#each-tools-own-memory). agnostic-ai never writes that store. To copy its facts into shared personal memory, run `agnostic-ai memory import --path ~/.qoder/projects//memory`. ## Import `import qoder` reads rules, agents, skill folders, commands, MCP servers, and portable settings fields. It reads `mcpServers` from project `.qoder/settings.json`, including JSONC, into the configured MCP source directory. Environment and header credentials become environment references. A server with a literal OAuth credential is left out, and import names the field to fix. Portable settings go to `settings/qoder.yaml`. Rule activation is kept. Hooks, personal settings, and the separate project `.mcp.json` are not imported. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # OpenHands (`openhands`) [OpenHands](https://docs.openhands.dev/overview/skills) gets `AGENTS.md`, agents and skills under `.agents/`, hooks, and a setup script. Add MCP servers in OpenHands itself. ## First rule Select `openhands` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. MCP servers have no output. Add them in Agent Canvas, as [MCP](#mcp) describes. For existing files, check [what `import openhands` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Open the project in OpenHands and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # pointer body + inlined always-on rules (shared path) .agents/agents/.md # one project agent (shared with Goose) .agents/skills//SKILL.md # one folder per skill or path-triggered rule (shared tree) .openhands/hooks.json # when hook entries exist .openhands/setup.sh # when an environment spec sets `install` ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Skills Written identically to the other `.agents/skills/` tools, so the shared folder is written once. The [OpenHands skills docs](https://docs.openhands.dev/overview/skills) document no field that keeps a skill user-only. The SDK reads `disable-model-invocation`, but such a skill then runs only when a `triggers` keyword appears anywhere in a message, not by name. Sync does not write it: `disable-model-invocation` is dropped with a note, and commands are skipped. ### Agents OpenHands loads `.agents/agents/.md` in local conversations. The writer is shared with Goose and writes `name`, `description`, an optional free-form `model`, and the prompt body. - Each server an agent names in `mcpServers` is written in full under `mcp_servers`, the only form OpenHands takes. Goose has no such key, so while Goose writes the same folder sync writes no servers and prints a note; set `outputs.openhands.agents-dir` to `.openhands/agents`. `x-openhands.mcp_servers` replaces the list. - A generic `tools` list is dropped with a sync note, because OpenHands uses its own tool names (`file_editor`, `terminal`). Set `x-openhands.tools` with those names instead. For an OpenHands profile that differs from Goose's, point `outputs.openhands.agents-dir` at `.openhands/agents`. - A portable `color` is dropped with a sync note, because [Goose's agent frontmatter](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/context-engineering/custom-agents.md) has no color. Set `x-openhands.color` to a [Rich color name](https://rich.readthedocs.io/en/stable/appendix/colors.html) to write it. ### Rules An always-on rule goes into the `## Rules` block of `AGENTS.md`. So does a rule with a catch-all `globs` or `paths`. A rule is always-on when it has no `globs` or `paths` and no scope from its folder or frontmatter. ### Path-triggered rules A scoped rule becomes `.agents/skills//SKILL.md` with a `paths:` list (its scope directory plus file patterns). OpenHands [loads it only when a matching file is touched](https://docs.openhands.dev/overview/skills/path). It uses the folder form (a flat `.md` also works), so these rules share `outputs.openhands.skills-dir` with skills. ### Hooks OpenHands [reads `.openhands/hooks.json` (`outputs.openhands.hooks-file`) per repository in Agent Canvas, OpenHands Cloud, and OpenHands Enterprise](https://docs.openhands.dev/openhands/usage/customization/hooks). [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) tests them before a session, using OpenHands's event data, shell, and timeout. Hooks load only from the working folder, so a root `.openhands/hooks.json` does not apply when OpenHands works in a subfolder. - Six events: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`. - Sync writes the Claude form (PascalCase keys inside a `{"hooks": {...}}` wrapper). OpenHands also accepts its own unwrapped snake_case form. - Each entry has `command`, `type` (always `command`), and optional `timeout` (seconds, default 60) and `async`. `matcher` applies only to the two ToolUse events. - A Claude matcher such as `Bash` matches nothing, because OpenHands calls that tool `terminal`. Sync adds a note and does not rename it. OpenHands documents only `terminal`, `*`, and regex. ### MCP No file. OpenHands keeps MCP servers in its [settings store](https://docs.openhands.dev/openhands/usage/settings/mcp-settings): add them in Agent Canvas under **Customize > MCP Servers**, or pass `mcp_config` to the SDK. It reads no project file, and ignores the `[mcp]` section of a project `config.toml` (legacy V0). - A project or home MCP spec gets one sync note that points at **Customize > MCP Servers**. - An agent's `mcpServers` still reach its `mcp_servers`, as [Agents](#agents) describes. There a remote `api_key` becomes an `Authorization: Bearer ` header, as [OpenHands sends](https://docs.openhands.dev/openhands/usage/settings/mcp-settings) it. An `Authorization` entry in `headers` wins. - `timeout` gets a sync note: sync does not map it into an agent's `mcp_servers`. Agent Canvas separately offers an optional SHTTP timeout in seconds in its [server editor](https://docs.openhands.dev/openhands/usage/settings/mcp-settings#server-fields). ### Environments `install` writes `.openhands/setup.sh`, the [repository setup script](https://docs.openhands.dev/openhands/usage/customization/repository) OpenHands runs each time it opens the repo. - The file has a `#!/bin/bash` line, the generated-file header, then `install` as written. OpenHands runs `chmod +x` itself, so sync does not set the executable bit. - `terminals` (Cursor's long-running dev processes) gets a sync note, because the script runs once when the repo starts. - With several environment specs, the last `install` wins, as for Cursor. ### Legacy config.toml and mcp.json The next sync removes a `config.toml` that an earlier release wrote. `import openhands` still reads the `[mcp]` table of a legacy `config.toml` into specs. Only the unmaintained OpenHands-CLI reads `~/.openhands/mcp.json`. `sync --global` no longer writes it. The next `sync --global` removes the servers sync recorded as its own there. That includes a hand-written server an earlier sync adopted because it matched a spec exactly. Sync also removes the file when it created it and nothing else is left. Other servers stay. Run `sync --global --backup` to keep a copy of the file first. `import --global openhands` still reads the file into specs. ## Config keys | Key | Default | | --- | --- | | `outputs.openhands.agents-dir` | `.agents/agents` | | `outputs.openhands.skills-dir` | `.agents/skills` | | `outputs.openhands.hooks-file` | `.openhands/hooks.json` | | `outputs.openhands.setup-file` | `.openhands/setup.sh` | `outputs.openhands.mcp-file` no longer has an effect. ## Import `agnostic-ai import openhands` reads the OpenHands files back into specs: | Source | Becomes | |--------|---------| | `AGENTS.md` embedded `## Rules` block (`### ` children) | `/.md` per rule | | `.agents/skills//SKILL.md` with `paths` (path-triggered rule) | `/.md` with `paths` | | `.agents/skills//SKILL.md` (+ bundled files) | `//SKILL.md` | | `.openhands/skills/` and `.openhands/microagents/` (legacy) | the same, read after `.agents/skills/` | | flat `.md` in any of those three directories | a rule, or a skill when it has `triggers` (kept under `x-openhands`) | | `.agents/agents/.md` | `/.md` | | `.openhands/hooks.json` | one hook spec per matcher group | | `config.toml` `[mcp]` table (legacy V0) | `/.yaml` per server | | `.openhands/setup.sh` | `/openhands-setup.yaml` with the script as `install` | | `AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md` | `.agents/skills/` wins a name clash with the legacy folders, as in OpenHands. Hooks import from both layouts. Snake_case `pre_tool_use` comes back as `PreToolUse`. Import loses these details (none change what OpenHands loads): - Unnamed `sse_servers` and `shttp_servers` entries are named after the URL host (`docs-example-test`). - A rule scoped by its folder comes back as the `paths` glob it was widened to. - An environment spec keeps only `install`. Its name and `terminals` are lost. ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Factory (`factory`) Factory [Droid](https://docs.factory.com/harness/subagents) reads the root `AGENTS.md` and custom droids in `.factory/droids/`. Factory has no per-rule directory, so rule bodies go into the `## Rules` block of `AGENTS.md`. ## First rule Select `factory` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Custom droids come from agent specs. A read-only droid also needs its MCP access restricted; sync supplies an empty server list unless you set one. For existing files, check [what `import factory` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Launch `droid` from the project and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # pointer body + rules (shared path) .factory/droids/.md # one custom-droid profile per agent .agents/skills//SKILL.md # one folder per skill (shared tree) .factory/commands/.md # one Markdown slash command per command spec .factory/hooks.json # when hook entries exist .factory/mcp.json # when MCP entries exist .factory/settings.json # when a settings entry carries a model, a shell-command rule, or an x-factory key ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Agents Each `.md` carries `name`, `description`, and optional `model` and `tools`. Any `x-factory` key passes through. - An agent with an empty body is skipped with a sync note, because Droid CLI requires a system prompt. - A portable `mcpServers` list is written as is. `mcpServers: []` excludes every MCP server, even global ones, so list the ones you want. See [`mcpServers`](https://agnostic-ai.org/docs/spec-format/agents/#mcpservers-support-by-target). - A portable `effort` becomes `reasoningEffort`, which accepts only `low`, `medium`, and `high`. `xhigh`, `max`, and integer budgets drop with a sync note. Factory ignores the field under `model: inherit`. See [per-target `model` and `effort`](https://agnostic-ai.org/docs/spec-format/agents/#per-target-model-and-effort). - `readonly: true` writes `tools: read-only`, Factory's read-only tool category. It replaces a portable `tools` list instead of narrowing it, with a sync note that points to `x-factory.tools` for a custom list. `x-factory.tools` wins over `readonly`. `readonly: false` does nothing. - A droid gets its MCP servers' tools, so a read-only droid with no `mcpServers` list gets `mcpServers: []`. A list you set is kept. ### Tools The generic `tools` list maps to Droid CLI's IDs (`Read`, `LS`, `Grep`, `Glob`, `Create`, `Edit`, `ApplyPatch`, `Execute`, `WebSearch`, `FetchUrl`). One unknown ID makes Factory reject the droid. - `Bash` becomes `Execute`, `Write` becomes `Create`, and `WebFetch` becomes `FetchUrl`, as in Factory's Claude Code importer. The rest already match. - `TodoWrite` and `Skill` drop silently, because every droid gets them. `ExitSpecMode` and `GenerateDroid` drop because custom droids cannot enable them. `tools: all` is never written, because an omitted key allows every tool. - Any other name drops with a sync note. The rest are still written. - `x-factory.tools` writes Factory's own names and wins over the translation. Only it can set a category (`read-only`, `edit`, `execute`, `web`, `mcp`) or an MCP tool ID. See [`tools` support by target](https://agnostic-ai.org/docs/spec-format/agents/#tools-support-by-target). ### Skills Written once to the tree that codex, amp, zed, and crush share. Factory also reads [`.agent/skills/**/SKILL.md`](https://docs.factory.com/harness/skills), which this adapter does not write. - Scoped skills go to `/.factory/skills//SKILL.md` with bundled files. Unscoped skills keep `.agents/skills/`. - `outputs.factory.skills-dir` replaces the directory at the root and in each scope. ### Commands `.factory/commands/.md` takes `description` and `argument-hint`. `$ARGUMENTS` is kept. Factory prefers skills for new workflows but still loads commands. ### Hooks `.factory/hooks.json` is the committed project file ([hooks docs](https://docs.factory.com/harness/hooks)). `sync` keeps the entries it did not write and replaces only its own. With `builtins: [memory]`, a `SessionStart` hook adds the [shared memory](https://agnostic-ai.org/docs/memory/) index to the session. - Nine events: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`, `SessionStart`, `SessionEnd`. - The file is keyed by event name, `{"": [{matcher, hooks: [...]}]}`, with no `hooks` wrapper (unlike Claude Code, Codex, Gemini, and Qoder). - Per entry: `type` (always `"command"`), `command`, and optional `timeout` in seconds (default 60). Factory's `commandRegex` has no spec field and is not written. - `matcher` is a regex over Factory's tool names, which match Claude's except `Bash`, `Write`, and `WebFetch`. A matcher that uses those is written as is, matches nothing, and raises a sync note. ### MCP `.factory/mcp.json` under `mcpServers`, the Claude Code and Cursor shape (stdio: `command`, `args`, `env`, no `type`; remote: `type`, `url`, `headers`). - `sync` rewrites the file whole. Factory's [MCP docs](https://docs.factory.com/harness/mcp) say to remove project servers by editing this file, but sync would undo that edit, so remove the MCP spec instead. - `disabled: true` becomes Factory's per-server boolean. See [`disabled` support by target](https://agnostic-ai.org/docs/spec-format/mcps/#disabled-support-by-target). - Both transports keep `disabledTools`, `timeout`, and `connectTimeout` (milliseconds), zeros included. Remote HTTP and SSE servers also accept `oauth: false` or an OAuth object with `scopes`, `resource`, `authorizationServerIssuer`, `clientId`, `clientSecret`, `clientMetadataUrl`, `tokenEndpointAuthMethod`, and `callbackPort`. - Set `x-factory.enabledTools` to load only the named tools. An empty list adds no restriction. `disabledTools` excludes named tools, including a name present in both lists. Sync preserves both lists, including explicit `[]`, without removing overlapping names. See [Factory tool filtering](https://docs.factory.com/harness/mcp#tool-filtering). - `x-factory` overrides each top-level option. A `type: ws` spec writes no server and raises a sync note, because Factory supports only stdio, HTTP, and SSE. ### Settings A portable `model` merges into `/.factory/settings.json`, the project level of Factory's [hierarchical settings](https://docs.factory.com/enterprise/hierarchical-settings-and-org-control). - A portable `effort` merges as top-level `reasoningEffort` ([CLI settings](https://docs.factory.com/droid-cli/settings)). Values are `none`, `dynamic`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`, and each model accepts a subset. Any other value raises a sync note. The managed `sessionDefaultSettings.reasoningEffort` is not written. - Sync merges. It sets only `model`, `reasoningEffort`, the command lists, and `x-factory` keys, so `disabledSkills` and other keys stay. - `x-factory` sets keys that have no portable field, mainly `sandbox` (file and process limits enforced by the operating system). Its command-list entries add to the translated ones: `x-factory.commandBlocklist: ["author-only"]` beside `deny: ["Bash(rm:*)"]` writes both. - Portable `allow`, `deny`, and `ask` become Factory's shell-command lists. `Bash(npm:*)` becomes `"npm *"` and `Bash(curl)` becomes `"curl"`. | portable | Factory key | why | |---|---|---| | `allow` | `commandAllowlist` | Always runs without approval. | | `ask` | `commandDenylist` | Always asks; you can approve. | | `deny` | `commandBlocklist` | Never runs. No approval. | - Factory's denylist only prompts, so portable `deny` maps to the blocklist. The denylist beats the allowlist, and the blocklist beats both. - Rules scoping a path, URL, or MCP tool (such as `Read(src/**)`) have no shell form and raise a sync note. - A list is written only when a rule translates into it. A list you keep by hand stays if no rule feeds it. If a rule does, sync replaces that key. - Factory deprecates the three lists in favor of `permissionRules` but still reads them ([LLM safety and agent controls](https://docs.factory.com/enterprise/llm-safety-and-agent-controls)). A permission rule needs a stable `id`, a `match.prefix` token list, and `tests.match` and `tests.noMatch` examples, which portable lists lack. Put them under `x-factory.permissionRules`, which is copied into `.factory/settings.json` as written. ### Handoff from a linked worktree With `handoff` or `handoff-hook` on, sync adds the main checkout's handoff store to `sandbox.filesystem.allowWrite` in `.factory/settings.local.json`, so Factory's [sandbox](https://docs.factory.com/autonomy-and-safety/sandbox) lets handoff saves and the automatic snapshot reach it. The file stays out of Git, and sync never turns the sandbox on. See [Write from a linked worktree](https://agnostic-ai.org/docs/handoff/#write-from-a-linked-worktree). ## Config keys | Key | Default | | --- | --- | | `outputs.factory.agents-dir` | `.factory/droids` | | `outputs.factory.skills-dir` | `.agents/skills` | | `outputs.factory.commands-dir` | `.factory/commands` | | `outputs.factory.hooks-file` | `.factory/hooks.json` | | `outputs.factory.mcp-file` | `.factory/mcp.json` | | `outputs.factory.conf-file` | `.factory/settings.json` | ## Import `agnostic-ai import factory` reads these back: | Source | Becomes | |--------|---------| | `AGENTS.md` embedded `## Rules` block (`### ` children) | `/.md` per rule | | `.factory/droids/.md` | `/.md` | | `.agents/skills//SKILL.md` and `.factory/skills//SKILL.md` | `//SKILL.md` | | `/.factory/skills//SKILL.md` | `///SKILL.md` | | `.factory/commands/.md` | `/.md` | | `.factory/mcp.json` (`mcpServers.`) | `/.yaml` | | `.factory/hooks.json`, else the legacy `.factory/hooks/hooks.json` | one hook spec per matcher group | | `.factory/settings.json` `model`, `reasoningEffort`, and command lists | `/factory.yaml`; `reasoningEffort` becomes `effort`, or `x-factory.reasoningEffort` when another settings spec sets a different effort | | `AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md` | - **Droids**: `tools` names map back (`Execute` to `Bash`, `Create` to `Write`, `FetchUrl` to `WebFetch`). A list with a category (`read-only`) or an MCP tool ID goes under `x-factory.tools` as written. `reasoningEffort` becomes `effort`. Other droid keys go under `x-factory`. - **MCP servers**: `enabledTools` imports under `x-factory.enabledTools`. Empty lists and names shared with `disabledTools` stay intact. - **Command lists**: these read back as `Bash(...)` rules: `commandAllowlist` to `allow`, `commandDenylist` to `ask`, `commandBlocklist` to `deny`. A pattern ending in ` *` becomes `Bash(x:*)`. Other `settings.json` keys stay in the file. ### Read-only droids on import A `tools: read-only` that came from a portable `readonly: true` also goes under `x-factory.tools`, because import does not turn it back into `readonly`. Syncing again writes the same droid file. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Kilo (`kilo`) Kilo [Code](https://kilo.ai/docs) gets `AGENTS.md`, agents, commands, plugin hooks, shared skills, and a merged `kilo.jsonc`. ## First rule Select `kilo` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Import covers only selected settings, ignore patterns, and generated rule sections. It does not preserve a complete hand-written Kilo setup. Review [import coverage](#import) and keep files outside that coverage before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Open the project in Kilo and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body + inlined rules (shared path) .kilo/rules/.md # one per rule the root AGENTS.md does not carry .kilo/agents/.md # one per agent .agents/skills//SKILL.md # one folder per skill, plus bundled assets (shared cross-tool tree) .kilo/commands/.md # one per command .kilo/plugin/.ts # one per hook, auto-registered at startup kilo.jsonc # instructions, mcp, and permission maps (merged with existing user config) .kilocodeignore # compatibility input for Kilo's permission migrator ``` ## Native mappings ### Field mappings, discovery, and compatibility ### Shell commands in global skills `sync --global` writes skills to `~/.kilo/skills/`. Kilo supports inline `` !`command` `` placeholders there and in its trusted `~/.agents/skills/` and `~/.claude/skills/` directories ([shell commands in skills](https://kilo.ai/docs/customize/skills#shell-commands-in-skills)). Kilo requests approval when loading the skill; `KILO_DISABLE_SKILL_SHELL` disables execution. Project and remote skills do not execute these commands. An arbitrary output directory does not become trusted because its source is global. The sync check accepts only inline shell placeholders at those user paths. `$ARGUMENTS`, positional arguments, and Claude's ` ```! ` blocks remain unsupported. Placeholders inside ordinary code blocks are examples. ### Agents (`.kilo/agents/.md`): - The filename sets the agent name, so `name:` is never written. Frontmatter carries `description` (falls back to the spec name) plus `color`, `mode`, and `model` when set. - `color` is not checked. Kilo accepts hex (`#FF5733`) or a theme token such as `primary`, `accent`, or `error` ([custom subagents](https://github.com/Kilo-Org/kilocode/blob/main/packages/kilo-docs/pages/customize/custom-subagents.md)), so `color: blue` may not show as you expect. See [`color` support by target](https://agnostic-ai.org/docs/spec-format/agents/#color-support-by-target). - `mode` takes OpenCode's `primary`, `subagent`, or `all`. - The portable `maxTurns` is written as `steps` ([agent options](https://kilo.ai/docs/customize/custom-subagents)). `x-kilo.steps` wins over it. - `disable`, `hidden`, `temperature`, and `top_p` need `x-kilo`, for example `x-kilo: {temperature: 0.1}`. - Kilo has no `tools:` key, so a `tools` list becomes a [`permission`](https://kilo.ai/docs/customize/agent-permissions) map: `tools: [Read, Grep]` writes `permission: {"*": deny, read: {...}, grep: allow, doom_loop: ask, external_directory: ask}`. The catch-all comes first because the last matching rule wins. `doom_loop` and `external_directory` ask at most, Kilo's default, so the catch-all does not silently deny paths outside the project. - Restricted agents retain project permission decisions for their listed tools and deny tools outside the list. Sync reads root `kilo.jsonc` and a hand-written `.kilo/kilo.jsonc`, including during preview. It does not read global user policy. Pattern intersections that cannot be represented exactly become denies, so a restricted agent can be stricter than the project. MCP server-name overlaps and Windows case differences are handled conservatively. Use `x-kilo.permission` for an explicit native override. - These names translate: `Read`, `Glob`, `Grep`, `Edit`, `Write`, `Bash`, `WebFetch`, `WebSearch`, `Task`, `Skill`, `TodoRead`, `TodoWrite`, and `mcp____` as `{server}_{tool}` ([permission keys](https://kilo.ai/docs/getting-started/settings/auto-approving-actions), [tool groups](https://kilo.ai/docs/automate/tools)). Other names drop with a sync note. If nothing translates, no map is written, because `{"*": deny}` alone would lock the agent out. `x-kilo: {permission: {...}}` wins outright. ### Skills go to the shared `.agents/skills//SKILL.md` tree. Kilo loads it by default beside its own `.kilo/skills/`. Codex, amp, zed, crush, openhands, windsurf, and augment share it. Kilo also scans `.claude/skills/` (in the VS Code extension, only with Claude Code Compatibility on). Any other `outputs.kilo.skills-dir` is added to `skills.paths` in `kilo.jsonc` ([skills](https://github.com/Kilo-Org/kilocode/blob/main/packages/kilo-docs/pages/customize/skills.md)). Your `skills.paths` entries and `skills.urls` stay. ### Rules - Unscoped rules go into the root `AGENTS.md`, which Kilo [always loads](https://kilo.ai/docs/customize/agents-md) ("cannot be individually disabled"). A rule already in that block gets no `.kilo/rules/` file or `instructions` entry, so it loads once. - A rule whose text differs for Kilo, such as one with a path variable, keeps its file. - Scoped rules use nested `AGENTS.md`. See [target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). - Kilo's [order](https://kilo.ai/docs/customize/agents-md) is agent prompt, then project `instructions`, then `AGENTS.md`, then global. Kilo still loads the legacy `.kilocode/rules/`, which this adapter never writes. ### No root rules block Kilo reads no rules block when a `file` output override moves the entry point off the root `AGENTS.md`, or when `AGENTS.md` is in `sync.unmanaged`. Check [the effect on other targets sharing that file](https://agnostic-ai.org/docs/target-behavior/#inlined-rules). Unscoped rules then get one file each under `.kilo/rules/`, listed by explicit path (not a glob) in the [`instructions`](https://kilo.ai/docs/customize/custom-rules) array of `kilo.jsonc`. When no rule file is left, sync removes only the `instructions` entries for rules that `AGENTS.md` now carries. Your own entries stay. ### Commands use Kilo's slash-command path ([workflows](https://github.com/Kilo-Org/kilocode/blob/main/packages/kilo-docs/pages/customize/workflows.md)). As with agents, the name comes from the filename. Frontmatter carries `description`, `agent`, `model`, `variant` (a reasoning-effort override such as `low` or `high`), and `subtask` when set, close to OpenCode's list. Kilo reserves the command name `goal` for its session goals ([goals](https://kilo.ai/docs/code-with-ai/agents/goals)): it ignores a command file by that name, warns, and offers it as `/goal:command`. A command spec named `goal` is still written, with a note to rename it. ### MCP servers merge into the `mcp` map of `kilo.jsonc` (not the deprecated `mcpServers`): - Stdio sets `type: "local"`, joins `command` and `args` into one `command` array, and uses `environment` for env vars. - Remote sets `type: "remote"` with `url` and `headers`. `oauth: false` turns off automatic OAuth. - `disabled: true` writes `"enabled": false`. An enabled server gets no key. - Both keep `timeout` (milliseconds, zero included). `x-kilo` can override `timeout` and `oauth`. - `sync` keeps your keys in `kilo.jsonc`. It reads JSONC, as Kilo documents, but drops comments and trailing commas with a warning. ### Hooks Kilo auto-registers each plugin module at startup ([plugins](https://kilo.ai/docs/automate/extending/plugins)). Its plugins work like [OpenCode](https://agnostic-ai.org/docs/targets/opencode/)'s, and sync uses the same code to generate both. `PreToolUse` and `PostToolUse` map to `tool.execute.before` and `tool.execute.after`. Other documented events use the `event` hook with an `event.type` guard. Matchers, exit code 2 blocking, and `disabled: true` behave the same. Only the module shape differs: `export default { id: "", server }`. Hooks on `shell.env`, `experimental.session.compacting`, or an unmapped event drop with a sync note. ### Settings a settings spec's default `model` and its `x-kilo` block (for Kilo-only keys such as `sandbox`) merge into the top level of `kilo.jsonc`. An `x-kilo` `permission` instead merges tool by tool with the translated rules. The portable `allow`, `deny`, and `ask` lists merge into the `permission` key of `kilo.jsonc` ([auto-approving actions](https://kilo.ai/docs/getting-started/settings/auto-approving-actions)): - Each rule becomes one glob under one tool key, matched against the tool's arguments. `Bash(npm run:*)` becomes `bash: {"npm run *": "allow"}`, `Bash(rm -rf /)` becomes `bash: {"rm -rf /": "deny"}`, `Read(docs/*)` becomes `read: {"docs/*": "allow"}`, a bare `Bash` becomes `bash: {"*": "allow"}`, and `mcp__github__list_issues` becomes `github_list_issues`. - A rule in two lists takes the stricter action. - `Write` and `Edit` rules both land under `edit`, the one permission Kilo's edit and write tools check (`packages/opencode/src/tool/write.ts`). So `Edit(.env)` in `deny` blocks writing `.env` too, even with a `Write` allow, and an agent that lists `Write` can also use Kilo's edit tool. - Inside one tool, patterns go allow, then ask, then deny. Kilo takes the last match, and Claude Code lets a deny or ask win over any allow, so a narrower allow never bypasses a broader deny. - `WebFetch(domain:example.test)` drops with a sync note, because Kilo matches URLs. Unknown tool names drop too. - `x-kilo.permission` writes Kilo's own map. It wins for the tool keys it names, and that spec's portable lists are skipped. - Your entries for tools agnostic-ai does not set stay. This adapter never writes `.kilo/kilo.jsonc`. A hand-written one merges over the root `kilo.jsonc` ([config order](https://github.com/Kilo-Org/kilocode/blob/main/packages/kilo-docs/pages/getting-started/settings/index.md#config-file-precedence)), so its `mcp` or `instructions` replace the generated ones. ### Ignore specs write `.kilocodeignore` in the project root. Kilo's [migrator](https://kilo.ai/docs/customize/context/kilocodeignore), not this adapter, turns it into read and edit denials. ## Config keys | Key | Default | Notes | |-----|---------|-------| | `outputs.kilo.rules-dir` | `.kilo/rules` | | | `outputs.kilo.agents-dir` | `.kilo/agents` | | | `outputs.kilo.skills-dir` | `.agents/skills` | | | `outputs.kilo.commands-dir` | `.kilo/commands` | | | `outputs.kilo.hooks-dir` | `.kilo/plugin` | Kilo only loads plugins from `plugin/` or `plugins/`, so moving this stops the hooks loading | | `outputs.kilo.mcp-file` | `kilo.jsonc` | | | `outputs.kilo.ignore-file` | `.kilocodeignore` | | With `builtins: [memory]`, sync adds both [shared memory](https://agnostic-ai.org/docs/memory/) indexes to `instructions`. While rules go into `AGENTS.md` (the default), your own entries stay, and dropping the built-in removes only those two. When sync lists rule files in `instructions`, it owns the whole list. With `memory.personal: repo`, the personal store lies outside the project root, and Kilo [ignores project-declared files there](https://github.com/Kilo-Org/kilocode/blob/v7.8.8/packages/opencode/src/session/instruction.ts). Sync writes only the shared index and prints a note. Add the store's `MEMORY.md` to `instructions` in `~/.config/kilo/kilo.jsonc` to load it. See [repo mode](https://agnostic-ai.org/docs/memory/#one-store-per-repository). ## Import `agnostic-ai import kilo` reads `.kilocodeignore` and two fields from the root `kilo.jsonc`: the default `model` (into `settings/kilo.yaml`) and portable entries in the `permission` map (into `settings/permissions-kilo.yaml`). Keys with no portable spelling (`external_directory`, `lsp`, `doom_loop`, namespaced MCP keys) stay in the file. Generated rule sections in `AGENTS.md` are recovered when their source specs are missing. Hand-written instructions, native rule directories, agents, skills, commands, MCP servers, and plugin hooks are not imported. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Jules (`jules`) Google [Jules](https://jules.google/docs) is a cloud agent that reads the root `AGENTS.md`. agnostic-ai writes only the shared instructions that point to source specs and the embedded `## Rules` block there. Google announced that Jules becomes read-only after November 11, 2026 and closes on December 31, 2026. Finish tasks before the read-only cutoff and follow the [official export instructions](https://jules.google/docs/changelog/#jules-turndown) to save your Jules configuration and history. ## First rule Select `jules` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Jules is sync-only: there is no importer or output for agents, skills, hooks, or MCP servers. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Before the read-only cutoff, make sure the generated `AGENTS.md` is committed to the branch Jules reads, then start a Jules task asking for a commit subject for a small fix; it should use `fix:`. Next, [compare another target](https://agnostic-ai.org/docs/compare/) if you also need skills, hooks, or MCP servers. ## Output ``` AGENTS.md # canonical entry-point pointer body + inlined rules (written by sync, shared path) ``` Jules has no project-level settings, so agnostic-ai writes no file for it beyond `AGENTS.md`. Jules must be enabled explicitly (see [Selecting targets](https://agnostic-ai.org/docs/configuration/#targets)). Agents, skills, hooks, and MCP are skipped with a warning. ## Config keys None. ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Goose (`goose`) Block [Goose](https://goose-docs.ai) reads the root `AGENTS.md` and a `.goosehints` file. Goose is opt-in. See [Selecting targets](https://agnostic-ai.org/docs/configuration/#targets). ## First rule Select `goose` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. Project MCP specs have no output for Goose. Configure servers in Goose itself. For existing files, check [what `import goose` reads](#import) before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Launch `goose` from the project and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body + inlined rules (shared path) .agents/agents/.md # one project agent (shared with OpenHands) .agents/skills//SKILL.md # one folder per skill (shared tree) .agents/plugins/agnostic-ai/plugin.json # plugin manifest, whenever a component lands in the plugin .agents/plugins/agnostic-ai/hooks/hooks.json .agents/plugins/agnostic-ai/skills//SKILL.md # only with skills-dir pointed at the plugin .goosehints # opt-in concatenated rules, only when rules-file is set .agents/REVIEW.md # root review instructions /.agents/REVIEW.md # directory-specific review instructions ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: are included in the `## Rules` block of `AGENTS.md`. `outputs.goose.rules-file: .goosehints` also writes all rules into one `.goosehints`, which Goose reads only with the `Developer` extension enabled ([using goosehints](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/context-engineering/using-goosehints.md)). - **Scoped rules**: Goose reads context files (`CONTEXT_FILE_NAMES`, default `AGENTS.md` and `.goosehints`) in the subdirectories it works in. Scoped rules go to a nested `AGENTS.md`, or with `rules-file` set, to `backend/.goosehints` for a `backend/` scope. Rules with the same scope share one file. - **Agents**: each file has `name`, `description`, an optional free-form `model`, and the prompt body. OpenHands reads the same path and fields, so the file is written once. A generic `tools` list is dropped with a sync note, because Goose does not document it. `x-goose` fields work. If they make the file differ from what another tool gets, set a different `outputs.goose.agents-dir`. - **Skills**: `.agents/skills/` is Goose's [recommended path](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/context-engineering/using-skills.md), read ahead of `.goose/skills/` and `.claude/skills/`. The other `.agents/skills/` tools, including Crush, get the same files, so the shared folder is written once. Shared rendering drops Crush invocation fields; separate its `skills-dir` to retain them. - Goose documents no field that keeps a skill user-only: it lists every skill for the model, and `/` loads one by hand ([using skills](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/context-engineering/using-skills.md)). So `disable-model-invocation` is dropped with a note, and commands are skipped. - **Hooks**: an [Open Plugins](https://goose-docs.ai/docs/guides/context-engineering/hooks/) package (`.agents/plugins/agnostic-ai/plugin.json` plus `hooks/hooks.json`). All 12 documented events are supported. `matcher` is a regular expression. `timeout` is in seconds (default 30). `x-goose.on_failure` takes `allow` or `block` for `PreToolUse` failures. [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) tests these hooks before a session, using Goose's event data, shell, and timeout. - **Plugin skills**: set `outputs.goose.skills-dir: .agents/plugins//skills` to bundle skills as a plugin ([plugins](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/context-engineering/plugins.md)), with or without hooks. Skills and hooks share one manifest. Goose prefixes names with the plugin name, so `review` in `agnostic-ai` loads as `agnostic-ai:review`. - **Reviews**: `goose review` reads `.agents/REVIEW.md` and `/.agents/REVIEW.md` from every directory with changed files and every parent directory, so root and scoped guidance combine ([v1.50.0 CLI reference](https://raw.githubusercontent.com/aaif-goose/goose/v1.50.0/documentation/docs/guides/goose-cli-commands.md)). Specs with the same scope are joined into one file, without frontmatter, because Goose reads plain text. Agent-style check files are not written. - **MCP**: skipped with a warning. ### Skills dir outside skills/ A skills directory inside a plugin folder but outside its `skills/` folder is not a Goose component and gets no manifest. Sync keeps an existing `plugin.json` it did not write and prints a note. Manifests recorded by an earlier sync remain managed. ## Config keys | Key | Default | Notes | |-----|---------|-------| | `outputs.goose.agents-dir` | `.agents/agents` | | | `outputs.goose.rules-file` | unset | opt-in, writes all rules into one `.goosehints` file | | `outputs.goose.skills-dir` | `.agents/skills` | point it at `.agents/plugins//skills` to bundle skills as a plugin; the manifest follows | | `outputs.goose.hooks-file` | `.agents/plugins/agnostic-ai/hooks/hooks.json` | overrides must keep the `/hooks/hooks.json` suffix | | `outputs.goose.review-file` | `.agents/REVIEW.md` | relative to each scope | ## Import `agnostic-ai import goose` reads the Goose files back into specs. Rules come from the inline block in `AGENTS.md`: | Source | Becomes | |--------|---------| | `AGENTS.md` embedded `## Rules` block (`### ` children) | `/.md` per rule | | `.goosehints` | the same, read only when `AGENTS.md` carried no rules | | `.agents/agents/.md` | `/.md`, unchanged except for the generated-file header | | `.agents/skills//SKILL.md` (+ bundled files) | `//SKILL.md` (folder copied unchanged) | | `.agents/plugins//skills//SKILL.md` | the same, for every plugin in the project | | `.agents/plugins//hooks/hooks.json` | one hook spec per matcher group, `on_failure` under `x-goose` | | `.agents/REVIEW.md` | `/review.md` | | `AGENTS.md` | `.agnostic-ai/AGNOSTIC_AI.md` | `.goosehints` is a fallback. With `rules-file` set, both files hold the same rules. Import reads every plugin, including hand-installed ones. `agnostic-ai import all` detects Goose only from `.goosehints`, `.agents/REVIEW.md`, or a plugin with `hooks/hooks.json` under `.agents/plugins/`. Antigravity plugins share `.agents/plugins/`, and OpenHands, Antigravity, and others share `.agents/agents/` and `.agents/skills/`, so those alone do not count. For a Goose project with only rules or plugin skills, run `agnostic-ai import goose` directly. Import loses the following, but the next sync still writes the same Goose files: - Only the rules block in the root file is read back, as with `import claude` and nested `CLAUDE.md` files. - Review specs with the same scope come back as one spec. - Scoped `/.agents/REVIEW.md` files are not read back. See [scoped context](https://agnostic-ai.org/docs/scoped-context/). ## Protected paths Not enforced. This target takes no settings specs, so sync reports a spec with a `protected` block as unsupported. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Augment (`augment`) [Augment Code](https://docs.augmentcode.com/setup-augment/guidelines) reads the root `AGENTS.md`, `.augment/`, and the shared `.agents/skills/` tree. ## First rule Select `augment` in `agnostic-ai.yaml`, then follow [First rule](https://agnostic-ai.org/docs/getting-started/#first-rule) to add the shared commit-prefix rule and sync it. With the default paths, it reaches `AGENTS.md`. MCP, hooks, and permission settings on this page apply to Auggie CLI; the IDE reads the project rules and skills. Already configured? [Import](#import) reads ignore patterns and selected permission rules only. Review the remaining native files before syncing. ## Verify Run `agnostic-ai sync --check` and inspect `AGENTS.md` for the commit-prefix rule. Open the project in Augment and ask for a commit subject for a small fix; it should use `fix:`. Next, [add a skill](https://agnostic-ai.org/docs/spec-format/skills/). ## Output ``` AGENTS.md # entry-point pointer body + rules (written by sync, shared path) .augment/ ├── rules/.md # one per rule the root AGENTS.md does not carry, and per agent_requested rule ├── agents/.md # one per agent ├── commands/.md # one per command; nested source scope becomes a namespace └── settings.json # mcpServers + hooks + toolPermissions, merged; only with MCP, hook, or settings specs .agents/skills//SKILL.md # one folder per skill (shared cross-tool tree) .augmentignore # workspace indexing exclusions .augment-guidelines # opt-in legacy joined rules, only when rules-file is set ``` ## Native mappings ### Field mappings, discovery, and compatibility - **Rules**: rule bodies go into the shared `## Rules` block of `AGENTS.md`. Augment also loads `.augment/rules/`. - An always-applied rule that is already in the block gets no `.augment/rules/` file. See [target behavior](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). - A spec with `alwaysApply: false` gets `type: agent_requested` and a `description` (falling back to the rule name). Rules have no `name` key. - **Legacy guidelines**: `outputs.augment.rules-file: .augment-guidelines` also writes all rules as one document. It is opt-in, because Augment cuts it first when context runs short. - **Agents** ([subagents docs](https://docs.augmentcode.com/cli/subagents)): `.augment/agents/.md` with `name` (required), `description` (falls back to the spec name), and, when set, `color` (written as is; Augment expects an ANSI color name) and `model`. - Augment's `tools` and `disabled_tools` take its own tool names (`view`, `codebase-retrieval`, `str-replace-editor`, ...), so the generic `tools` field raises a sync note. Set `x-augment: {tools: [...]}` or `x-augment: {disabled_tools: [...]}` instead. - **Skills**: Augment also scans `.claude/skills/` and `.augment/skills/`. - **Commands**: `.augment/commands//.md`, with a directory scope from the source folder as a nested folder name. `description`, `argument-hint`, and `model` stay in frontmatter. - **Ignore**: ignore specs write `.augmentignore`. `import augment` restores a hand-written one, keeping pattern order and negation. ### Entry point off AGENTS.md Moving the entry point off root `AGENTS.md` with a `file` override, or listing it in `sync.unmanaged`, makes every rule keep a `.augment/rules/` file. The default `always_apply` is left out. Before making the shared file unmanaged, check [its effect on other targets](https://agnostic-ai.org/docs/target-behavior/#inlined-rules). ### MCP Servers merge into `.augment/settings.json` under `mcpServers`. Stdio uses `command`, `args`, and `env` with no `type`. Remote uses `type: http` or `type: sse` plus `url` and `headers`. This is the Auggie CLI project file Augment recommends for team-shared servers ([config docs](https://docs.augmentcode.com/cli/config)), not the IDE Settings Panel. - Sync changes only its own keys. `shell`, `startupScript`, `theme`, plugin keys, and tool permissions stay. - If the file is JSONC, sync drops comments and trailing commas and prints a one-line warning. Keys stay, comments do not. - Augment documents no per-server `disabled` key, so `disabled: true` is dropped with a sync note. Use `auggie mcp remove`. - Transports are `stdio`, `sse`, and `http` only ([integrations docs](https://docs.augmentcode.com/cli/integrations)). A `type: ws` spec writes no server and raises a sync note. ### Hooks Hooks merge in under `hooks`, in the same write as `mcpServers`. Supported events: `PreToolUse`, `PostToolUse`, `Stop`, `SessionStart`, `SessionEnd` ([hooks docs](https://docs.augmentcode.com/cli/hooks)). [`agnostic-ai hook run`](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) tests them before a session, using Auggie's event data, shell, and timeout. - `timeout` is in **milliseconds**: the spec's seconds times 1000 (vendor default 60000). - `command` must be a script ending in `.sh`, `.ps1`, `.cmd`, or `.bat`, because Augment never runs an inline shell string. Other commands are written as is with a sync note. A hook with `args` is skipped with a note, because Augment has no shell to fold them into. - `matcher` is optional on `PreToolUse` and `PostToolUse` (vendor default `.*`) and left out on the session events. - Matchers use Augment's tool names (`launch-process`, `str-replace-editor`, `save-file`, ...). A Claude-style matcher (`Bash`, `Write`, ...) matches nothing and raises a sync note. ### Shared memory With `builtins: [memory]`, sync adds an executable `.augment/hooks/memory-augment-session-start.sh` and a `SessionStart` entry for Auggie CLI on its [supported platforms](https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli#system-requirements): macOS, Linux, and Windows WSL. The script finds the project from its working directory and returns both memory indexes through `hookSpecificOutput.additionalContext`. The same script is installed for `sync --global`. The IDE keeps the memory rule. See [shared memory](https://agnostic-ai.org/docs/memory/#load-at-session-start) for the context limit and how to keep the tool's own memory. ### Permissions Settings specs merge into the same file under `toolPermissions`, which Auggie CLI and Cosmos cloud agents follow. Augment recommends committing `.augment/settings.json` so the policy applies to every cloud agent in the repo ([permissions docs](https://docs.augmentcode.com/cli/permissions)). - Rules are an ordered array and the first match wins, so deny rules come first. - Each `permission` is an **object** such as `{ "type": "deny" }`. Augment ignores a bare-string permission. - Bare tool names map onto Augment's six tools (`Read` to `read`, `Bash` to `terminal`, `WebFetch` to `web-fetch`, ...). - `Bash(prefix:*)` becomes a `terminal` rule with `shellInputRegex: ^prefix`. An exact `Bash(command)` is anchored at both ends. `mcp____` becomes `{tool-name}_{server-name}`. These are not written and raise a sync note: - `model`: the file has no documented key. - The `ask` list: none of Augment's types (`allow`, `deny`, `webhook-policy`, `script-policy`) prompts. - Path- or URL-scoped rules such as `Read(src/**)`: only `terminal` takes a matcher, and a bare `read` would cover every file. `x-augment.toolPermissions` writes Augment's rule objects as is, ahead of the translated ones. Other `x-augment` keys, such as `shell` or `startupScript`, merge in as written. ## Config keys | Key | Default | Notes | |-----|---------|-------| | `outputs.augment.rules-dir` | `.augment/rules` | | | `outputs.augment.agents-dir` | `.augment/agents` | | | `outputs.augment.skills-dir` | `.agents/skills` | | | `outputs.augment.commands-dir` | `.augment/commands` | | | `outputs.augment.ignore-file` | `.augmentignore` | | | `outputs.augment.rules-file` | unset | opt-in, writes the legacy `.augment-guidelines` document | | `outputs.augment.mcp-file` | `.augment/settings.json` | also the hooks and permissions file, because all three merge into one document | ## Import `import augment` reads `.augmentignore` and the `toolPermissions` policy in `.augment/settings.json` for Augment's six tools. It skips `shellInputRegex` entries (no portable form) and bare-string `permission` entries (invalid in Augment). Nothing else is imported. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). # Ellipsis (`ellipsis`) [Ellipsis](https://www.ellipsis.dev/docs/code-review) runs repository reviews from `.ellipsis/code-review.yaml` on the default branch. Sync builds that file from review specs and a target-specific settings spec. Installing the GitHub App, publishing files, creating secrets, and running sessions remain separate steps. ## First review Select `ellipsis` in `agnostic-ai.yaml`. Add a [review spec](https://agnostic-ai.org/docs/spec-format/reviews/) with one useful check for this repository, then name its reviewer in `.agnostic-ai/settings/ellipsis.yaml`: ```yaml x-ellipsis: pull_requests: draft: false reviewer: name: project-review claude_code: model: claude-opus-5-5 budget: session: 3 ``` The reviewer needs a name, one engine (`claude_code`, `codex`, or `gemini`), and an explicit model supported by Ellipsis. The review prompt comes from the review spec, not the agent specs or a settings `prompt` field. Preview with `agnostic-ai sync --dry-run`, then run `agnostic-ai sync` and `agnostic-ai sync --check`. A review spec and complete reviewer produce `.ellipsis/code-review.yaml`. Missing requirements produce a note with the key to fix. Existing native files? Check [import coverage](https://agnostic-ai.org/docs/targets/ellipsis-reference/#import) before syncing. Sync replaces a hand-written file at its output path. ## Output | Source | Default output | |---|---| | Review specs and reviewer settings | `.ellipsis/code-review.yaml` | | Selected environment, setup, and MCP specs | `.ellipsis/environments/.yaml` | The review-only example needs no environment file. Rules, agents, skills, and hooks have no Ellipsis output. Directory-specific review checks become conditional text in the prompt, rather than separate reviewers or enforced path filters. ## Keep the files in Git Ellipsis reads the default branch, so commit the generated review file. If managed Git ignores are enabled, allow its outputs: ```yaml gitignore: enabled: true commit: [ellipsis:reviews] ``` This also keeps an environment named by that review trackable. Independent environments use `ellipsis:environments` or `ellipsis:mcps`. See [generated-file ownership](https://agnostic-ai.org/docs/configuration/#gitignore). ## Protected paths Not enforced. Put path-specific checks in a review spec and verify that they trigger on relevant pull requests. Portable `permissions` and `protected` settings have no Ellipsis effect. ## Verify Install the [Ellipsis GitHub App](https://www.ellipsis.dev/docs/code-review) and publish `.ellipsis/code-review.yaml` to the default branch. Open a pull request that violates the chosen check, then one that satisfies it. Confirm one review per push and no config errors. Without an explicit output override, Ellipsis comments on failure and stays silent on a pass; it does not approve automatically. These real review trials have not been run for this adapter. Local tests cover written files and previews only. Next, [add an environment or MCP server](https://agnostic-ai.org/docs/targets/ellipsis-reference/#environment-and-mcp-servers) if the review needs setup commands or external tools. Store referenced secrets in Ellipsis before running a session; sync does not create them. # Ellipsis native reference Start with [one Ellipsis review](https://agnostic-ai.org/docs/targets/ellipsis/). This reference covers optional environment setup, MCP servers, native fields, and import limits. ## How specs map to the file | Source | In the file | |--------|-------------| | Review specs with no scope | The start of the reviewer's `prompt` | | A review spec scoped to `services/api` | A later section of the same prompt | | `x-ellipsis.ellipsis` | `ellipsis`. `kind` is always `code_review`; `name` defaults to `code-review`; `description`, `enabled` (`false` pauses the file), and `metadata` are copied as written | | `x-ellipsis.pull_requests` | `pull_requests`, copied as written | | `x-ellipsis.reviewer` | The one entry of `reviewers`, with the `prompt` filled in from the review specs | | `x-ellipsis.output` | `output`, copied as written | | `x-ellipsis.budget` | `budget`, copied as written | The reviewer needs a `name`, one engine block (`claude_code`, `codex`, or `gemini`), and that block's `model`. Ellipsis names the model for every session, so none is guessed. Other native reviewer keys, such as `paths`, `environment`, `skills`, `permissions`, `budget`, and `comments`, pass through. A later settings spec replaces each top-level `x-ellipsis` block it sets (`ellipsis`, `pull_requests`, `reviewer`, `output`, `budget`) as a whole. Nothing is added for you. With no `output` block, Ellipsis stays silent on a pass and comments on a failure, and never approves. Automatic approval needs `output.on_pass: approve` in your settings. The prompt comes only from review specs. A `prompt` inside `x-ellipsis.reviewer` is an error, because the file would then have two sources for it. Agent specs never become a reviewer. ### Directory-specific checks A scoped review follows the root guidance in the same prompt, under a heading that says when it applies: ```yaml reviewers: - name: project-review claude_code: prompt: |- Flag any handler that talks to the database directly instead of going through a repository. ## Checks for changes under services/api/ Apply the checks in this section only when the pull request changes a file under `services/api/`. Skip them otherwise. Reject a change that removes or renames a field of a public response. model: claude-opus-5-5 ``` Sections follow in sorted directory order. The condition is text the model reads. Ellipsis has its own reviewer-level `paths` filter, which starts or skips a whole reviewer. agnostic-ai does not use it for scopes, because that needs one reviewer per directory. Check on a real pull request that Ellipsis applies the scoped checks as written before you depend on them. ## Environment and MCP servers Ellipsis starts each session from the environment the session names. A review names it with the reviewer's `environment` key, and sync sets that key to the selected environment's name. A `x-ellipsis.reviewer.environment` that names another environment stops the review file, because the specs would disagree. With no `x-ellipsis.environment`, the key passes through as written, so an environment you manage by hand still works. Set `x-ellipsis.reviewer.environment: null` to keep the reviewer in Ellipsis' basic sandbox: sync then writes no `environment` key, with or without a selection. An environment selection writes `.ellipsis/environments/.yaml`, even without portable environment or MCP specs. It needs a safe name and one repository. With no selection, environment and MCP specs produce a note instead of a file. An invalid selection keeps an already-generated environment in place and the review continues to name it. Multiple possible generated environments stop sync until you repair the selection. Conflicting scripts or an output path outside `.ellipsis/` also prevent new output. ### Setup scripts Ellipsis runs setup in four stages. Each script starts in `/sandbox`, the parent of the checkout, under `bash -eo pipefail`. | Source | Stage | Runs | |--------|-------|------| | `install` | `hooks.build_base` | The first time, and when a file in its `inputs` changes. With no `inputs`, on every commit | | `setup` | `hooks.after_checkout` | Once for each commit a session needs that is not prepared yet | | `x-ellipsis.environment.hooks.before_start` | `hooks.before_start` | Every time a session starts or resumes | | `x-ellipsis.environment.hooks.validate` | `hooks.validate` | Every time a session starts or resumes, last and never cached. A failure stops the session, and the pull request environment check runs it too | Sync writes `install` and `setup` after `cd /sandbox/`, so they run in the repository checkout and not in its parent. Commands that read a script in the repository, such as `bash scripts/setup.bash`, work as they do locally. A script you write under `x-ellipsis.environment.hooks` goes in as written, with no `cd`. Setting both `install` and `hooks.build_base.run`, or both `setup` and `hooks.after_checkout`, stops the file; keep one source for each stage. `hooks.build_base.inputs` lists the files whose change reruns `build_base`. Each is an exact file starting with the repository name, such as `api-repo/package-lock.json`. Globs, `..`, and files outside the repository stop the file. With `inputs` set, Ellipsis places only those files in the checkout while `build_base` runs, so list every file the install reads. `setup-windows`, `cleanup`, `dev-commands`, and `terminals` have no Ellipsis stage or key. Sync reports each as having no effect. To run a service, start it in the background from `setup` and wait until it is ready. ### Repository, variables, and compute These Ellipsis options have no portable spec, so they live under `x-ellipsis.environment` and are written as given: ```yaml x-ellipsis: environment: name: api-environment repository: name: api-repo # without the owner; checked out at /sandbox/api-repo owner: acme ref: main description: API checkout variables: - name: NPM_TOKEN # no value: Ellipsis reads the stored secret of that name - name: NODE_ENV value: test compute: cpu: 2 memory: 8GB timeout: 30m hooks: build_base: inputs: [api-repo/package.json, api-repo/package-lock.json] before_start: | cd /sandbox/api-repo test -d node_modules validate: command -v jq # fails the session if jq is missing mcp_servers: [slack] # connected integrations, listed by name ``` A variable under a credential name, such as `NPM_TOKEN`, or with a value that looks like a credential, such as `postgres://admin:pw@db/app`, may not carry a `value`: the file is committed, so the secret stays in Ellipsis. Names Ellipsis rejects (`ANTHROPIC_`, `OPENAI_`, `CODEX_`, `GEMINI_`, `GOOGLE_` and `CLAUDE_CODE_USE_` prefixes, and three exact names) stop the file. ### MCP servers Ellipsis reads MCP servers from the environment file and ignores a repository's `.mcp.json`, so sync writes the servers there and writes no `.mcp.json` for this target. Each [MCP spec](https://agnostic-ai.org/docs/spec-format/mcps/) becomes an `mcp_servers` entry, sorted by name after any integrations: | Spec | Entry | |------|-------| | `type: http` with `url` and `headers` | A remote server over streamable HTTP, with `url` and `headers` | | `stdio` with `command`, `args`, and `env` | A server that runs inside the sandbox, with `command`, `args`, and `env` | A `${NAME}` in `env` or `headers` stays `${NAME}`, which Ellipsis fills from the stored secret `NAME` when a session starts. A plain setting needs `!literal` in the spec, as for every target. Local secret values never reach the file or a preview. A `${NAME}` in `url` or `args`, or a `${NAME:-default}` anywhere, has no Ellipsis form, so sync leaves out that server with a note, or that header or variable. Sync skips a server it cannot preserve and reports its name and the reason. Under `on-unsupported: error` it stops instead: - `sse` and `ws` transports. Ellipsis connects over streamable HTTP only. - `oauth`, which an entry has no key for. - `cwd` on a stdio server. Write the script path as `/sandbox//`. - A stdio command or argument that is a path from your machine: `./`, `../`, `~`, a Windows drive, `/Users/...`, `/home/...`, `${workspaceFolder}`, or a script file name with no directory. It must start with `/sandbox//`, and may not name another checkout or climb out of it with `..`. - A name outside letters, digits, `-`, and `_`, or `slack` or `linear`, which Ellipsis reads as integrations. - `disabled: true`. Ellipsis has no switch, so the server is left out, which has the same effect. A server's `description`, `timeout`, and `roots` have no key and get a no-effect note. To give a server different values in the sandbox, set them under `x-ellipsis` on the MCP spec, which replaces the top-level key for this target only. An `env` or `headers` value under `x-ellipsis` must be a `${NAME}` reference, because the `!literal` mark works only on the top-level maps; a literal there leaves the server out: ```yaml command: node args: [scripts/mcp-server.js] x-ellipsis: args: [/sandbox/api-repo/scripts/mcp-server.js] ``` A stdio server runs in the sandbox, so install what it needs in `install`. Sync cannot check that a command exists there. On a Codex reviewer, a server that fails to start fails the whole turn, not just that server's tools. ## Config keys | Key | Default | Notes | |-----|---------|-------| | `outputs.ellipsis.review-file` | `.ellipsis/code-review.yaml` | Ellipsis reads only this path from the default branch, so move it only for a trial | | `outputs.ellipsis.environment-file` | `.ellipsis/environments/.yaml` | Must be a `.yaml` file under `.ellipsis/` | ## Settings fields Only `x-ellipsis` reaches the file. The portable `model`, `permissions`, and `protected` fields of a settings spec have no key in a review file, so sync reports each one as having no effect. A key in `x-ellipsis` that is not one of `ellipsis`, `pull_requests`, `reviewer`, `output`, `budget`, or `environment` stops the files, because Ellipsis rejects unknown fields. The inner keys of `pull_requests`, `output`, `budget`, and the reviewer are copied without checking, so a typo there fails in the Ellipsis config check on the pull request. See [Keep the files in Git](https://agnostic-ai.org/docs/targets/ellipsis/#keep-the-files-in-git) before deploying either output. ## Import `agnostic-ai import ellipsis` reads a hand-written `.ellipsis/code-review.yaml` back into specs: | Source | Becomes | |--------|---------| | The reviewer's `prompt` | `/review.md` | | Everything else in the file | `/ellipsis.yaml`, under `x-ellipsis` | A hand-written `.ellipsis/environments/*.yaml` with `ellipsis.kind: environment` imports too: | Source | Becomes | |--------|---------| | A `build_base` or `after_checkout` script that starts with the line `cd /sandbox/` | `install` or `setup` in `/ellipsis.yaml`, without that line | | Any other script, `inputs`, `before_start`, `validate` | `x-ellipsis.environment.hooks`, as written | | The name, repository, description, metadata, variables, compute, and `slack` or `linear` entries | `x-ellipsis.environment` in `/ellipsis-environment.yaml` | | Each server with `url` or `command` | `/.yaml` | The environment, its servers, and its setup are scoped to `targets: [ellipsis]`, so no other tool changes. A plain `env` or `headers` value stays as written and is marked `!literal`, because Ellipsis reads every `${NAME}` as a stored secret and a missing secret fails the session. A credential becomes a `${NAME}` reference, and import prints the secret name to set in Ellipsis. A credential in a server's `url` or `args` leaves the file as written, since Ellipsis reads references in `env` and `headers` only. When the reviewer has no `environment` and an environment file imports too, import sets `x-ellipsis.reviewer.environment: null`, so the review keeps running in the basic sandbox and does not move into the imported environment. The next sync writes the same behavior back. Comments and key order in the hand-written files are not kept. A file that carries the generated header imports nothing, since its specs are the source. Import stops with a message and writes nothing when the review file has more than one reviewer, is not a `code_review` file, has a field Ellipsis does not list, or has a reviewer without a prompt. Keep one reviewer and import again, or leave the file hand-written and list it under [`sync.unmanaged`](https://agnostic-ai.org/docs/configuration/#syncunmanaged). An environment file it cannot express is left as written, with a note that names the reason: more than one repository, a field the schema does not list, a variable that holds a credential value, a credential in a server's `url` or `args`, or a server with both `url` and `command`. Several environment files are left too, since sync writes one. When the file is not at `.ellipsis/environments/.yaml`, import says so, because sync writes there and Ellipsis would see two files with one name. `import all` detects the target from a `.ellipsis/` folder. ## Not covered - More than one reviewer, more than one environment or repository, and the organization file in the `.ellipsis` repository. - Cache planning beyond the `inputs` you list, scheduled agents, and skills. - Installing the GitHub App, creating secrets, billing, and session logs. - Rules, agents, skills, and hooks. Sync reports them as unsupported for this target. # Grok Build (`grok`) Select Grok explicitly and keep its project instructions visible to Git. Grok skips ignored rule files, so it is excluded from the default targets. ## First rule Create `agnostic-ai.yaml`: ```yaml version: 1 targets: [grok] gitignore: enabled: true commit: [grok:instructions] ``` `agnostic-ai init` writes the `commit` line when it selects Grok with the managed `.gitignore` on. Without it, `lint` reports [LINT053](https://agnostic-ai.org/docs/cli-reference/lint-codes/) and sync stops for every tool. Create `.agnostic-ai/rules/style.md`: ```markdown --- name: style --- Use Conventional Commits for commit subjects. ``` Run `agnostic-ai sync`, then `agnostic-ai sync --check`. Read `.grok/rules/style.md` and run `git check-ignore --no-index .grok/rules/style.md`: it should report no matching ignored path. A root or global ignore pattern can still hide the file. Sync rejects that case before emitting Grok output; remove the matching pattern yourself. Explicitly disabling agnostic-ai's ignore management is also supported when your own ignore policy keeps the rules visible. The check uses Grok's root `.gitignore` and global ignore file, not nested ignore files or `.git/info/exclude`. It conservatively requires parent directories to be re-included before a file below an ignored directory. Install and open the CLI using the [official setup guide](https://docs.x.ai/build/overview). Grant project trust through Grok, then verify the rule in a session. Sync does not grant trust or run Grok. ## Output | Spec | Native path | |---|---| | Rule | `.grok/rules/.md` | | Directory rule | `/.grok/rules/.md` | | Agent | `.grok/agents/.md` | | Skill and resources | `.grok/skills//SKILL.md` and sibling files | | Command | `.grok/commands/.md` | | Command hook | `.grok/hooks/.json` | | Shared hook scripts | `.grok/hook-scripts/` | | MCP and permission lists | `.grok/config.toml` | Scoped skills, agents, and commands use the corresponding directory below their source scope. Rules apply when Grok discovers that directory while walking from its working directory to the repository root. File glob conditions have no native rule equivalent; use a whole directory scope. Output overrides use `outputs.grok.dir`, `rules-dir`, `agents-dir`, `skills-dir`, `commands-dir`, `hooks-dir`, `mcp-file`, and `conf-file`. `conf-file` separates permission settings from the MCP destination. A custom destination must still be a location Grok reads. Rule visibility checks include custom paths. ## Native mappings Agents carry `name`, `description`, `model`, native `tools`, `disallowedTools`, `skills`, `effort`, `maxTurns`, `background`, and the supported native fields under `x-grok`. Effort accepts `low`, `medium`, `high`, `xhigh`, or `max`. Portable `can` and `tools` use verified native allowlist aliases and disable unrequested MCP bridge tools. Explicit bridge IDs and `x-grok.tools` retain their native choices; a capability without an exact mapping fails with the agent name. Unknown native tool entries fail because Grok restores its full toolset for an unresolved name. Empty restrictions are rejected because native empty tools inherit all tools. To preserve an existing Grok restriction, use verified native aliases or IDs in `tools` or `x-grok.tools`. Skills keep `name`, `description`, `when-to-use`, `paths`, `allowed-tools`, `argument-hint`, `user-invocable`, `disable-model-invocation`, `metadata`, `license`, and `compatibility`. Bundled files are copied unchanged. `allowed-tools` describes use and does not enforce permission. `user-invocable: false` also hides a skill from model discovery. Skill model and effort overrides are not applied by the verified contract. Grok converts skill and command names to lowercase `a-z`, digits and hyphens, with a 64-character limit. Skills can fall back to their folder name. Sync rejects names with no valid native form before writing files. MCP servers become `[mcp_servers.]`. Stdio keeps `command`, `args`, `env`, and `cwd`. Remote servers keep `url`, `headers`, and explicit HTTP or SSE transport. `disabled` becomes the inverse native `enabled`. Native `startup_timeout_sec` and `tool_timeout_sec` stay seconds. Environment references remain text for Grok to expand. Native extras under `x-grok` include the source-backed authentication and timeout fields. Portable `description` and `timeout` produce coverage notes because they have no exact native mapping. Project settings write only `permissions.allow`, `permissions.ask`, and `permissions.deny` to `[permission]`; `x-grok.permission` and `x-grok.plugins` carry native project configuration. Existing MCP servers and unrelated configuration keys are retained. Project model, effort, permission default mode, and session retention are unsupported. Ignore, review, and environment specs have no native output. At user scope, portable [voice settings](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) write `ui.voice_capture_mode` and `ui.voice_stt_language`; Grok has no dictation on/off key. Hooks use native PascalCase `event` and optional `matcher`, command text, safely quoted `args`, timeout in seconds, and per-handler `env`. Sync adds `AGNOSTIC_AI_TARGET=grok` to generated handlers. Grok provides `CLAUDE_PROJECT_DIR` and `GROK_WORKSPACE_ROOT`. A matcher on `Stop` or `UserPromptSubmit` has no effect and produces a note. Hook errors fail open; native explicit denies block according to the event. Portable `on`/`match` hooks are not translated in this adapter. Use Grok-native events and its documented reply protocol. Project hook execution also requires Grok's hook trust approval. On Windows, shared script paths that need shell-specific quoting, such as paths with spaces, require an explicit native wrapper command. Sync refuses an automatic rewrite it cannot preserve. Existing native invocation commands keep their quoting. ## Import Run `agnostic-ai import grok` before the first sync of a native project. It reads root and scoped rule, agent, skill, and command directories, resources, native hook JSON files, MCP tables, compact permission lists, and plugins. Plugins are retained under `x-grok.plugins` from the configured settings destination. `x-grok.hooks` preserves each native hook file's groups and handler fields. Unsupported hook handler types fail import. Imported standalone MCP credentials become environment references through the shared credential filter. Inline agent MCP and hook metadata keep their native form. Import refuses literal credentials there, and in standalone hooks, before writing the source file, by the [setting value rule](https://agnostic-ai.org/docs/spec-format/mcp-import/#setting-values). Hook `env` values are passed through literally, so putting `${TOKEN}` there does not read the process environment. Inherit credentials from the process and reference them in hook `command` or `url`, where Grok expands variables. Import errors identify the native path without printing credential values. Import uses the configured output directories. Importing emitted files into fresh source directories, syncing again, linting, and running `sync --check` are covered by the adapter tests. ## Global configuration `agnostic-ai sync --global --only grok` writes `$GROK_HOME/AGENTS.md`, `skills/`, `agents/`, `hooks/`, `hook-scripts/`, and `config.toml`. `GROK_HOME` defaults to `~/.grok` and must be absolute when set. Global commands and permission lists are not emitted. The portable global model maps to `[models].default`; global effort is unsupported. Global MCP servers and the default model share one planned config write. Sync owns individual servers and the model key, preserves other text and private file modes, checks for concurrent edits, and removes only unchanged owned values. `agnostic-ai import --global grok` reads the supported settings, MCP, and hook surfaces. Literal credentials are skipped by global import. ## Protected paths Not enforced. Use an instruction rule or explicit permission rules. See [protected-path alternatives](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). ### Native evidence and runtime limits Paths and fields were checked against the [official project rules](https://docs.x.ai/build/features/project-rules), [MCP](https://docs.x.ai/build/features/mcp-servers), [hooks](https://docs.x.ai/build/features/hooks), [subagents](https://docs.x.ai/build/features/subagents), [skills](https://docs.x.ai/build/features/skills-plugins-marketplaces), and [settings](https://docs.x.ai/build/settings) documentation, plus public source snapshot [2bdd1d6a](https://github.com/xai-org/grok-build/tree/2bdd1d6a6369de0e8c68132ea4539e9abd9e14a8). The source snapshot is an independent format reference, not proof of exact binary source parity. Validation uses native-format fixtures and isolated filesystem tests; it does not install, authenticate, approve trust, or run a model session. Grok also reads compatibility files from other enabled tools, so those can contribute additional instructions, hooks, or servers. # Mistral Vibe (`vibe`) Sync writes the files [Mistral Vibe Code CLI v3.0.3](https://github.com/mistralai/mistral-vibe/releases/tag/v3.0.3) reads. Start Vibe from the project root and make its trust choice yourself. The normal CLI loads project configuration only for trusted roots. In host sandbox or agent-socket mode, Vibe reads project instructions and skills but ignores project `.vibe` configuration, agents, and hooks. ## Set up this tool Add `vibe` to your `targets` list. This example creates a model setting and a native hook that denies a tool call through Vibe's JSON response: ```sh mkdir -p .agnostic-ai/settings .agnostic-ai/hooks cat > agnostic-ai.yaml <<'YAML' version: 1 targets: [vibe] YAML cat > .agnostic-ai/settings/model.yaml <<'YAML' model: devstral-small YAML cat > .agnostic-ai/hooks/guard.yaml <<'YAML' name: guard target: vibe event: pre_tool x-vibe: match: 're:bash|process_start' command: | printf '%s\n' '{"decision":"deny","reason":"Review this command first"}' YAML agnostic-ai sync --dry-run agnostic-ai sync agnostic-ai sync --check ``` The model must already exist in Vibe's model catalog or user configuration. Unknown model names fall back to Vibe's default. Sync does not create a model definition or change the primary agent. ## Native paths | Spec | Project output | User output with `sync --global --only vibe` | |---|---|---| | Rules | `AGENTS.md`, with scoped instructions in deeper `AGENTS.md` files | `~/.vibe/AGENTS.md` | | Agents | `.vibe/agents/.toml` | `~/.vibe/agents/.toml` | | Skills and bundled files | `.vibe/skills//SKILL.md` | `~/.vibe/skills//SKILL.md` | | MCP servers | `.vibe/config.toml`, `[[mcp_servers]]` | `~/.vibe/config.toml`, one entry per `name` | | Hooks | `.vibe/hooks.toml`, `[[hooks]]` | `~/.vibe/hooks.toml`, one entry per `name` | | Model settings | `.vibe/config.toml`, `active_model` | `~/.vibe/config.toml`, `active_model` | `VIBE_HOME` moves every user path in this table. Project output overrides use `file`, `rules-file`, `agents-dir`, `skills-dir`, `conf-file`, `mcp-file`, and `hooks-file` under `outputs.vibe`. `conf-file` moves the shared model and MCP file unless `mcp-file` names a separate file. An override changes the generated path; configure Vibe to read a path outside its discovery locations. Commands use the shared conversion to skills invoked by the user. Vibe has no native command file emitted by this target. ## Agents and skills Agents emit as `agent_type = "subagent"`, with inline `instructions`, a description, and an optional `active_model`; a subagent runs on that model only from v3.0.3. A native `x-vibe.agent_type: agent` preserves a primary profile for explicit selection with `vibe --agent `. Vibe 3.0.3 reserves the agent name `main`, in any case, for hook matching and does not load a file with that name, so sync stops and asks you to rename the agent. Import notes such a file. Portable agent tools map `Bash`, `Read`, `Write`, and `Edit` to `bash`, `read_file`, `write_file`, and `edit`. `Grep` has no direct mapping in Vibe 3.0; the native agent searches through its shell tool. Unresolved tools are omitted with a coverage note while supported tools keep the agent restricted. Sync fails if none of the requested tools can be mapped. Use `x-vibe.enabled_tools`, `x-vibe.disabled_tools`, and native `x-vibe.tools` permission tables for Vibe-specific policies. Empty `enabled_tools` would allow every tool, so sync rejects it. Portable `readonly` and permission patterns have no direct mapping. Vibe's `safety` field controls presentation, not access. Skills keep `user-invocable` and `disable-model-invocation`, plus bundled files. Names use lowercase letters, digits, and single hyphens, at most 64 characters. Descriptions have at most 1024 characters. Vibe reads `allowed-tools` as descriptive metadata and does not enforce it. ## Connections, settings, and hooks MCP names keep their original spelling. Vibe compares case-sensitive identities formed by replacing each character outside ASCII letters, digits, `_`, and `-` with `_`, stripping edge underscores and hyphens, then taking 256 characters. Empty or colliding identities stop sync and import. MCP servers support stdio and streamable HTTP. HTTP headers use native static authentication under `[mcp_servers.auth]`. Preserve OAuth or environment-backed authentication with `x-vibe.auth`; for example, `type: static` and `api_key_env: DOCS_TOKEN`. Vibe reads that variable name at runtime. Arbitrary environment references in URLs, arguments, headers, and stdio environment values have no verified expansion and raise coverage notes. SSE and WebSocket servers are omitted with a note. `model` maps to `active_model`. Other native project settings pass through `x-vibe`. Portable reasoning effort and permission settings raise coverage notes; Vibe's reasoning setting belongs to each model. At user scope, portable [voice settings](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) write `voice_mode_enabled`; Vibe's experimental dictation has no mode or language key. Hooks accept native `event: pre_tool`, `post_tool`, or `post_agent`. `matcher` maps to native `match`; `timeout` is in seconds. Use `x-vibe.strict: true` on tool hooks to deny or clear output when a hook fails. On `post_agent`, `match` runs the hook only after the named agent ends its turn, and `match: main` selects the main agent (Vibe 3.0.3 and later). `post_agent` rejects `strict: true`. A shell guard should match both `bash` and `process_start`. Hooks receive JSON on standard input and respond with Vibe JSON. Exit zero with `{"decision":"deny","reason":"..."}` denies according to the event: `pre_tool` blocks a call, `post_tool` rewrites its output, and `post_agent` requests a retry. A nonzero exit is a hook failure. Claude-style exit 2 and portable lifecycle events are not translated. Commands receive `AGNOSTIC_AI_TARGET=vibe`; arguments fold into the command because Vibe has no separate argument field. On Windows, arguments PowerShell cannot preserve cause an error; write the complete native command in that case. ## Protected paths Not enforced. Sync cannot write an edit-blocking rule or hook for this tool, so it prints a [protected-path coverage note](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). ## Import and ownership Run `agnostic-ai import vibe` before syncing handwritten project files. It imports instructions, agent TOML, skill folders, MCP entries, model and native settings, and hook TOML. Native fields stay under `x-vibe`. Import refuses literal credentials in native settings, profiles, and connection fields; keep credentials in user configuration or use a native environment-variable setting. An agent that references a system prompt without inline instructions stops import; copy that prompt into `instructions` first. Global sync adopts equal handwritten entries without rewriting them. Omitted `sampling_enabled` compares as `false`, Vibe's native default; a handwritten `true` conflicts unless the spec sets `x-vibe.sampling_enabled: true`. If an earlier sync adopted `sampling_enabled: true` from a spec that omitted sampling, sync stops before changing the file. Set `x-vibe.sampling_enabled: true` to keep it or `false` to disable it. It changes and removes only its recorded MCP and hook entries, preserving other entries, comments, and file permissions. A hand-edited owned entry stops sync. An equivalent inline array can be adopted, but changes require native `[[mcp_servers]]` or `[[hooks]]` tables. `import --global vibe` keeps unmanaged connections and hooks and excludes entries still equal to managed state. See [global configuration](https://agnostic-ai.org/docs/global-configuration/) for source layout and commands. # Kimi Code CLI (`kimi`) Sync generates native files for [Kimi Code CLI 2.3.1](https://github.com/MoonshotAI/kimi-code/releases/tag/%40moonshot-ai/kimi-code%402.3.1), the maintained `@moonshot-ai/kimi-code` package. Run Kimi from your synced Git project root. Its own workspace trust choice controls project MCP loading. Select `kimi` explicitly. It is excluded from the default targets because its native agent tool lists cannot express scoped shell restrictions such as `Bash(git diff *)`. This keeps existing projects working with the default targets. ## Set up this tool ```sh mkdir -p .agnostic-ai/rules .agnostic-ai/agents cat > agnostic-ai.yaml <<'YAML' version: 1 targets: [kimi] YAML cat > .agnostic-ai/rules/changes.md <<'MD' --- name: changes --- Keep changes small and explain failures. MD cat > .agnostic-ai/agents/reviewer.md <<'MD' --- name: reviewer description: Review a patch can: [read] --- Read the patch and report defects. MD agnostic-ai sync --dry-run agnostic-ai sync agnostic-ai sync --check ``` ## Native paths | Spec | Project output | User output with `sync --global --only kimi` | |---|---|---| | Rules | `.kimi-code/AGENTS.md`, scoped `/.kimi-code/AGENTS.md` | `~/.kimi-code/AGENTS.md` | | Agents | `.kimi-code/agents/.md` | `~/.kimi-code/agents/.md` | | Skills and resources | `.kimi-code/skills//SKILL.md` | `~/.kimi-code/skills//SKILL.md` | | MCP servers | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` | | Hooks | Explicit profile export only | `~/.kimi-code/config.toml`, `[[hooks]]` | | Settings | Explicit profile export only | Owned scalar keys in `~/.kimi-code/config.toml` | `KIMI_CODE_HOME` relocates all user paths together. Sync never changes the older `~/.kimi` data directory or runs Kimi's migration. Kimi-specific instructions and ordinary `AGENTS.md` instructions are additive. Kimi loads instructions from the Git root through the working directory. The uppercase plain filename wins over `agents.md` in one directory. Kimi-only projects use the branded path. When a configured peer already emits the plain `AGENTS.md`, Kimi shares that file and its scoped instruction files so common text is delivered once. Target-specific rules must be compatible with every reader of a shared file. See [shared entry points](https://agnostic-ai.org/docs/configuration/#entry-point-files) for instruction content and path overrides. Agents and skills are discovered from the nearest Git root, including linked Git worktrees. Kimi's branded MCP file is relative to the session working directory. When launching from a subdirectory, explicitly choose a repository-root MCP mirror if required. ### Output overrides | Key | Default | |---|---| | `agents-dir` | `.kimi-code/agents` | | `skills-dir` | `.kimi-code/skills` | | `mcp-file` | `.kimi-code/mcp.json` | | `root-mcp-file` | Unset, opt-in repository-root mirror | | `rules-file` | Unset, optional concatenated instructions | | `conf-file` | Unset, explicit data-root `config.toml` export | Overrides change generated paths. Kimi must be configured to discover a relocated file. Skills remain at project root; nested skill scopes are refused. ## Agents and skills Agents use YAML frontmatter and a Markdown prompt. Portable capabilities map to Kimi's `Read`, `Write`, `Edit`, `Bash`, `FetchURL`, and `WebSearch`. `Glob` and `Grep` pass through. An explicit empty tool list denies every tool; an omitted list is unrestricted. Scoped tool rules such as `shell(git diff *)` cannot be represented by this allowlist and stop sync. Native `whenToUse`, `override`, `disallowedTools`, `subagents`, and tool patterns stay under `x-kimi`. Replacing a built-in agent requires an explicit native `override: true`. Agent `model`, `effort`, and `maxTurns` have no effect in this Kimi release and raise coverage notes. Portable `readonly` has no direct mapping. Skills preserve their supporting files and map `disable-model-invocation` to `disableModelInvocation`. Native `type`, `whenToUse`, and `arguments` can be set under `x-kimi`. Kimi supports manual invocation as `/skill:`; standalone command specs are unsupported. Portable skill tool restrictions and model fields have no native enforcement. ## MCP connections MCP supports `stdio`, `http`, and `sse`. A disabled server emits `enabled: false`, retaining its override of a same-name user server. Native `enabledTools: []` exposes no tools. Keep native timeouts in milliseconds, tool filters, OAuth configuration, and `deferred` under `x-kimi`; deferral also depends on native runtime and model support. Stdio servers preserve `x-kimi.executor: local` and `x-kimi.runtime_id`. Sync and import reject every other executor, including `kaos`, removed in Kimi Code 2.2.0. Remove that option or choose `local` when the server should run locally. Sync cannot choose that execution location for you. Kimi inherits the process environment and copies configured strings literally. It does not expand general `${NAME}` references in connection fields. A portable `Authorization: Bearer ${TOKEN}` maps exactly to native `bearerTokenEnvVar: TOKEN`. Other unresolved references omit the whole server, so a missing credential never produces an unauthenticated connection. Write `$${NAME}` to request literal text. Sync never reads the named secret or starts a server. Kimi merges the user MCP file, repository-root `.mcp.json`, then working-directory `.kimi-code/mcp.json`; later same-name entries replace earlier entries. The adapter writes only its branded project file by default. ## Selected settings and hooks {#selected-profile} Global sync maps `model` to an existing native `default_model` alias and accepts boolean `x-kimi.default_plan_mode` and `x-kimi.telemetry`. It edits only those owned scalar keys and native hook records, preserving other user text and tables. Global effort is unsupported because `[thinking].effort` needs separate leaf ownership; it is available in an explicit profile export. To export settings and hooks, opt into a separate Kimi data root: ```yaml outputs: kimi: conf-file: kimi-profile/config.toml ``` ```sh agnostic-ai sync -t kimi KIMI_CODE_HOME="$PWD/kimi-profile" kimi ``` This selects the entire separate data root, including its agents, skills, connections, and credentials. It is not a project settings layer. Configure that root's native model/provider settings yourself. To place the other generated resources there too, set their output paths explicitly. In this export, `model` writes `default_model`, `effort` writes `[thinking].effort`, and native tables pass through `x-kimi`, including `permission` and `secondary_model`. Portable permission and protected-path fields are unsupported. Native hooks accept only `event`, optional `matcher`, `command`, and optional integer `timeout` from 1 to 600 seconds. There are no native `env` or `cwd` configuration fields. Arguments fold into the command; unsafe Windows argument folding stops sync. Hooks receive `AGNOSTIC_AI_TARGET=kimi` through their command wrapper. Portable hook lifecycle/decision translation is unsupported; use native Kimi events and response formats. Native events are `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionResult`, `UserPromptSubmit`, `UserPromptQueued`, `TurnStarted`, `Stop`, `StopFailure`, `Interrupt`, `SessionStart`, `SessionEnd`, `SessionHeartbeat`, `SubagentStart`, `SubagentStop`, `TaskStarted`, `PreCompact`, `PostCompact`, and `Notification`. ## Protected paths Not enforced. Sync reports portable protections as unsupported. Native permission rules in an explicit profile constrain individual Kimi tools and do not provide a general filesystem boundary for Bash or MCP tools. ## Import and ownership Run `agnostic-ai import kimi` before syncing handwritten native files. Import reads the branded instructions, Markdown agents, folder and flat skills, MCP file, and the explicitly configured TOML profile. It preserves safe native fields under `x-kimi`, including empty restrictions and disabled connections, and refuses literal credentials. Folder skills win over same-name flat skills. Native files linked outside the project are refused for agents and flat skills; resource folders follow the shared contained-folder importer. `agnostic-ai import kimi --global` imports the selected user's model, hooks, and MCP connections. Project and global hook imports check native records and commands inside generated wrappers for literal credentials before writing hook specs. Global sync records ownership so removing the corresponding specs restores user text and leaves unrelated hooks and tables intact. ## Sources - [Configuration files](https://moonshotai.github.io/kimi-code/en/configuration/config-files) - [Subagents](https://moonshotai.github.io/kimi-code/en/customization/agents) - [Skills](https://moonshotai.github.io/kimi-code/en/customization/skills) - [Released loaders and schema](https://github.com/MoonshotAI/kimi-code/tree/684bae9ea6a437f3bae98bba1dcb797a917d500b/packages/agent-core-v2/src) --- # Cross-target behavior One spec can become different files for each tool. A rule becomes `.claude/rules/.md` for Claude Code and a section of `AGENTS.md` for Codex. Tools also differ in how they find files, in permissions, and in the fields they support. [Compare targets](https://agnostic-ai.org/docs/compare/) shows sample output; `agnostic-ai compare` checks your own specs. ## Directory-specific instructions A rule's `scope` limits it to a directory, through native file conditions or nested instruction files. Tools differ in support and in when they load the rule: some at the repository root, some while editing. Check [directory-specific instructions](https://agnostic-ai.org/docs/scoped-context/) before you share scoped rules. ## Entry-point files Sync copies `.agnostic-ai/AGNOSTIC_AI.md` into each selected tool's instruction file. The next sync overwrites edits to those copies. See [entry-point files](https://agnostic-ai.org/docs/configuration/#entry-point-files) for what to write in the source. | Instruction file | Targets | |---|---| | `CODEBUDDY.md` | [CodeBuddy Code](https://agnostic-ai.org/docs/targets/codebuddy/) | | `AGENTS.md` | Codex, Amp, Warp, Cline, Windsurf, Junie, Kiro, Crush, Trae, Jules, Goose, Augment, Qoder, OpenHands, Factory, Kilo, OpenCode, Mistral Vibe | | `CLAUDE.md` | Claude Code | | `GEMINI.md` | Gemini CLI | | `QWEN.md` | [Qwen Code](https://agnostic-ai.org/docs/targets/qwen/) | | `CONVENTIONS.md` | Aider | | `.github/copilot-instructions.md` | Copilot | | `.rules` | Zed | | `.agents/AGENTS.md` | Antigravity | | `.kimi-code/AGENTS.md` | Kimi Code CLI | Tools that share a path get one write. Junie also has its preferred `.junie/AGENTS.md`. Claude can import identical shared instructions from `AGENTS.md`. Cursor and Continue get rule files, not a root instruction file. Each [target page](https://agnostic-ai.org/docs/targets/) explains how that tool finds its files. ### Inlined rules Some tools read no separate rule directory. Sync puts their project-wide rules below the shared body in a marked `## Rules` block. Augment and Kilo use that block too, though they have rule directories. OpenHands uses it for always-on rules; path-triggered rules become skills. Qwen Code uses it for rules without file patterns; a rule with `globs` goes to `.qwen/rules/` and loads when a matching file is read or edited. Cline, Kiro, Qoder, Kilo, and Augment skip a native rule file when its body already appears in the root `AGENTS.md` they read. File-conditioned rules and tool-specific bodies keep their own files. Keep `AGENTS.md` enabled in Cline's Rules panel and Qoder's context file list, or those tools miss the shared rules. Trae keeps every rule file because reading `AGENTS.md` is optional. Windsurf keeps them for legacy compatibility. Enabling those tools beside an inlining target can load the same rule twice. A tool whose entry point moves off root `AGENTS.md` keeps its separate rule files. Listing `AGENTS.md` in `sync.unmanaged` stops writes for every target that shares the file, not for one tool. The next full sync removes obsolete generated rule files, even edited ones that still carry the generated header. Before that cleanup, `sync --check`, `doctor`, and `status` report `ledger` drift; `doctor --fix` removes it. ### Shared settings files Warp global voice settings use the same per-key ownership in its platform settings file; see [Warp voice dictation](https://agnostic-ai.org/docs/targets/warp/#voice-dictation). In mergeable files such as `.claude/settings.json`, `.gemini/settings.json`, `.cursor/cli.json`, and `opencode.json`, sync records the keys it owns and keeps the rest. A generated key you left unchanged goes away when no spec produces it. A value you edited stays as an orphan; `doctor --fix` asks before removing it. Sync tracks MCP servers in mergeable maps by name. A spec with the same name can replace your server and print a note. Name tracking does not apply to files sync rebuilds whole, such as Claude's `.mcp.json` or Cursor's `.cursor/mcp.json`. Check the target's import and overwrite behavior before the first sync. When a target leaves the setup, cleanup removes only its recorded values. Sync deletes a shared settings file only when it created the file and nothing else remains. Invalid or edited orphan files stay for you to review. ### Other entry-point options A partial sync still refreshes shared instruction files for all configured readers, so the body matches a full sync. For the [legacy merged rules file](https://agnostic-ai.org/docs/configuration/#entry-point-files) and [`sync.target-overview`](https://agnostic-ai.org/docs/configuration/#synctarget-overview), see configuration. ## Spec kind behavior The [tool support table](https://agnostic-ai.org/docs/targets/#capability-matrix) shows what sync writes, not that the native feature matches in full. Sync can still leave out native fields, a client can require trust, and a shared prompt does not always enforce a restriction. | State | What to do | |---|---| | Native | Use the target's own format; check field and client limits on its page. | | Mapped | Check what feature carries the content. An agent prompt written as a skill is not a native subagent. | | Opt-in | Enable the named output setting, then check native discovery. A legacy file can be written without the tool loading it. | | Source only | Keep the portable source; no default native artifact is written. A pointer does not activate the feature. | | No output | Remove that target from the spec or use a supported native route. Sync warns by default. | Each [spec reference](https://agnostic-ai.org/docs/spec-format/) has the details. [`on-unsupported`](https://agnostic-ai.org/docs/configuration/#on-unsupported) sets what sync does with an unsupported kind. Coverage notes name the fields a tool converts or leaves out. ### Shared skills and agent discovery Many targets share `.agents/skills/`, and sync writes identical folders once. Crush and Goose share neutral skill frontmatter; Crush commands remain skipped at that shared path because their user-only flag cannot be kept. [`sync.shared-skills`](https://agnostic-ai.org/docs/configuration/#syncshared-skills) can replace identical copies in other directories with symlinks. A tool can also find folders written for another selected target, even when the spec excludes that tool. **Devin discovers `.agents/agents/` profiles written for Goose, OpenHands, and Antigravity.** Those profiles can lack Devin's `allowed-tools`, which defaults to all tools. A `.devin/agents/` profile with the same name can also appear. `targets:` controls which adapter writes a file, not which tools read it, so check the profiles Devin finds for duplicate names and tool limits. Sync reports which targets write shared profiles. See [Windsurf / Devin](https://agnostic-ai.org/docs/targets/windsurf/). ### Hooks loaded through another tool Cursor, Copilot, and Trae can read `.claude/settings.json` beside their own hook files, so a hook shared with Claude can run twice. Cursor reads it by default; Trae only when you opt in. Copilot has no switch to stop these reads. The target pages give the fix: [Cursor](https://agnostic-ai.org/docs/targets/cursor/#third-party-reads), [Copilot](https://agnostic-ai.org/docs/targets/copilot/#hooks), [Trae](https://agnostic-ai.org/docs/targets/trae/). When both targets are selected, a portable hook's Claude copy exits under Cursor. Review other hooks yourself. Cursor can also ignore a Claude hook's `args` and run a bare interpreter; use a shell-form command or turn off compatibility reads. Event names, tool matcher names, timeout units, and failure behavior differ by tool. Use [portable events](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) where a tool supports them, and test the native event before you rely on a guard. ## Related - [Shared memory](https://agnostic-ai.org/docs/memory/): one store every tool reads, beside each tool's own memory. - [Target selection](https://agnostic-ai.org/docs/configuration/#targets): which tools sync writes by default. - [Global output](https://agnostic-ai.org/docs/global-configuration/#global-output): user-level files and the variables that move a tool's config folder. - [Adding adapters](https://github.com/Chemaclass/agnostic-ai/blob/main/docs/internal/adding-adapters.md), or [open an issue](https://github.com/Chemaclass/agnostic-ai/issues) to request a target. --- # Configuration `agnostic-ai.yaml` lives at the project root, and commands read it from the current directory. Every section is optional. The old name `agnostic.config.yaml` still loads with a warning; `agnostic-ai migrate` renames it. ## Minimal project config ```yaml version: 1 targets: [claude, codex] gitignore: enabled: true ``` `agnostic-ai init` creates one with your selected tools. A **target** is an AI coding tool that receives generated files. **Sources** are the specs you edit; **outputs** are the files those tools read. After you change the config, run `agnostic-ai sync --dry-run` to see the planned output, then `agnostic-ai sync` and `agnostic-ai sync --check`. In scripts and coding agents, name the tools instead of relying on a prompt. [Top-level fields](#top-level-fields) lists every key. For which value wins, see [precedence](#precedence) and [layered specs](#layered-specs). For directory-specific instructions, give a rule a `scope`; see [scoped context](https://agnostic-ai.org/docs/scoped-context/). ## Local overrides `agnostic-ai.local.yaml` holds changes for one machine. A single value or a list replaces the base value. A group of settings merges key by key. `init` adds the file to `.gitignore`. Personal specs go in [`.agnostic-ai/local/`](https://agnostic-ai.org/docs/local-overrides/). ```yaml # agnostic-ai.local.yaml (never committed) on-unsupported: error outputs: claude: dir: .claude-local # overrides base; rules-file from base survives ``` ## Editor validation `init` adds this comment so editors validate the file against the schema of your release. ```yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/Chemaclass/agnostic-ai/v0.72.0/docs/schemas/config.schema.json ``` ## Top-level fields An unknown key, in `agnostic-ai.local.yaml` too, fails every command that reads the config with [AAI-004](https://agnostic-ai.org/docs/errors/#aai-004-config-decode-failed). The error names the file, line, and closest known key. | Field | Type | Default | Description | |-------|------|---------|-------------| | `version` | int | `1` | Schema version. Any value other than `1` fails with [AAI-004](https://agnostic-ai.org/docs/errors/#aai-004-config-decode-failed). | | [`requires`](#requires) | string | none | Releases the specs work with: a minimum, one release, or a range. | | [`sources`](#sources) | map | `.agnostic-ai//` | Source directories. | | [`targets`](#targets) | list | see [targets](#targets) | Tools to write files for. | | [`builtins`](#built-ins) | list | none | Built-in specs to enable. `init` enables `handoff`. | | [`outputs`](#outputs) | map | per target | Output path overrides. | | [`models`](#models) | map | none | Model tiers that specs name. | | [`on-unsupported`](#on-unsupported) | string | `warn` | What to do with a spec kind a tool lacks. | | [`gitignore`](#gitignore) | map | `enabled: false` | Managed `.gitignore` block. | | [`sync`](#sync) | map | see section | Sync behavior. | | [`verify`](#verify) | map | disabled | A project command that `verify` runs. | | [`import`](#import) | map | per source | Import behavior. | | [`lint`](#lint) | map | see section | Budgets for always-loaded text. | | [`doctor`](#doctor) | map | see section | Opt-in diagnostic checks. | | [`coverage`](#coverage) | map | none | Coverage notes to accept, and whether notes fail sync. | | [`memory`](#memory) | map | `personal: checkout` | Where personal memory lives. Set it in `agnostic-ai.local.yaml` only. | ## Built-ins `builtins` turns on specs bundled with the binary. None are on unless listed; `init` writes `builtins: [handoff]` for a new project, or `[handoff, memory]` with `--memory`. ```yaml requires: ">=0.80.0" builtins: [handoff] ``` | Name | Adds | Setup and supported tools | |---|---|---| | `handoff` | A skill to leave and resume a task | [Session handoffs](https://agnostic-ai.org/docs/handoff/) | | `handoff-hook` | Automatic Git snapshots and startup notices | [Automatic snapshots](https://agnostic-ai.org/docs/handoff/#add-automatic-git-snapshots) | | `memory` | Shared project and personal memory | [Shared memory](https://agnostic-ai.org/docs/memory/) | | `rtk` | A Claude Code hook that shortens command output; install RTK separately | [RTK setup](https://agnostic-ai.org/docs/rtk-and-caveman/#turn-them-on) | | `caveman` | An on-demand response skill from a fixed upstream version; no separate program needed | [Caveman setup](https://agnostic-ai.org/docs/rtk-and-caveman/#turn-them-on) | Before you enable RTK or Caveman, [check for an existing copy](https://agnostic-ai.org/docs/rtk-and-caveman/#if-you-already-use-rtk-or-caveman). Unknown names fail as AAI-004 and list the valid names. A project, pack, or personal spec with the same kind and name wins over the built-in. The same list works in the [global home config](#global-configuration). A `builtins` list in a local config replaces the shared list, and `builtins: []` there turns them all off. When you remove a name, the next sync removes the files and tool settings it manages, and keeps unrelated settings and project overrides. A local `builtins` list never changes the committed ignore files; see [block contents](#block-contents). When you enable built-ins, set `requires` to at least 0.80.0 so an older binary cannot skip them. An upgrade that changes built-in text makes `sync --check` fail until you sync. ## `memory` `memory.personal` sets where the `memory` built-in keeps personal memory. It is one user's choice, so set it in `agnostic-ai.local.yaml`; in `agnostic-ai.yaml` it fails as AAI-004. | Value | Personal memory folder | |-------|----------------| | `checkout` (default) | `.agnostic-ai/local/memory/` in each checkout. | | `repo` | `~/.agnostic-ai/local/memory/-/`, one folder that every worktree of the repository shares (under `$AGNOSTIC_AI_HOME` when set). | ```yaml # agnostic-ai.local.yaml memory: personal: repo ``` In a Git project with managed ignores, [`init --memory`](https://agnostic-ai.org/docs/cli-reference/start/#init) writes `memory.personal: repo` to a new `agnostic-ai.local.yaml`. See [one store per repository](https://agnostic-ai.org/docs/memory/#one-store-per-repository) for what sync writes and which tools can save there. ## `requires` Pick one form: ```yaml requires: ">=0.71.0" # minimum: 0.71.0 or any newer release requires: "0.73.0" # exact: only 0.73.0; "=0.73.0" means the same requires: ">=0.73.0 <0.74.0" # range: 0.73.0 and its patch releases ``` - **Minimum**: the default that `init` writes, naming the release that created the project. Newer releases keep working. - **Exact**: choose it when you commit generated files and run `sync --check` in CI, since a newer release can write different output. `init --gitignore=off` writes a minimum and prints this advice. When npm installs the tool, pin the same release in `package.json`. - **Range**: accept patch releases but not the next minor one. [`upgrade --requires`](https://agnostic-ai.org/docs/cli-reference/upgrades/#upgrade-options) raises the value to the installed release and keeps its form: `>=0.71.0` becomes `>=0.74.0`, and `0.73.0` becomes `0.74.0`. A range becomes a minimum, and the command prints the value it replaced. A binary outside the value stops every command that reads your specs, before any write, with [AAI-005](https://agnostic-ai.org/docs/errors/#aai-005-installed-version-outside-requires). That page lists the fixes. `sync --watch` stops when a pulled config puts the binary outside `requires`. A value is one or more terms separated by spaces, and the binary must match every term. A term is `>=X.Y.Z`, `=0.90.0` to accept newer releases. A build from source (`go run`, or a commit after a tag) is not a release, so it warns once instead. `agnostic-ai.local.yaml` can replace the value. An empty `requires:` there turns the check off. The [global home config](#global-configuration) accepts the key too. ### Older releases Releases before 0.70.0 ignore the key. Releases before 0.74.0 read only `>=X.Y.Z` and stop on an exact or range value with AAI-004. Install the release the value names instead of editing `requires`. ## `sources` Sync skips missing directories. Paths follow the [path rules](#path-semantics). A source root can be a directory symlink (macOS, Linux) or junction (Windows); sync does not follow links below it. | Field | Default | Description | |-------|---------|-------------| | `agents` | `agents` | `*.md` agent specs. | | `skills` | `skills` | `*.md` skill specs (or nested `/SKILL.md`). | | `rules` | `rules` | `*.md` rule specs. | | `hooks` | `hooks` | `*.yaml` hook specs. | | `mcps` | `mcps` | `*.yaml` MCP server specs. | | `commands` | `commands` | `*.md` saved prompts. | | `settings` | `settings` | `*.yaml` permissions and defaults. | | `reviews` | `reviews` | `*.md` review guidance. | | `environments` | `environments` | `*.yaml` worktree and sandbox setup. | | `ignore` | `ignore` | `*.md` context exclusion patterns. | ## `outputs` `outputs..*` overrides where one target writes. An unknown field fails with AAI-004. Each target page lists its keys and defaults, starting at the [targets index](https://agnostic-ai.org/docs/targets/). [Claude Code](https://agnostic-ai.org/docs/targets/claude-reference/#claude-settings) and [Codex](https://agnostic-ai.org/docs/targets/codex-reference/#codex-config) also accept settings blocks. ```yaml outputs: claude: rules-dir: .claude/rules cursor: mcp-file: .cursor/mcp.json ``` `outputs..agents: skill` writes agents as on-demand skills on Amp, Crush, Warp, and Zed, which have no subagents. See [agents as skills](https://agnostic-ai.org/docs/spec-format/agents/#agents-as-skills). `outputs..provenance-header: false` drops the `Generated by agnostic-ai` line from the files that target writes. A shared root file such as `AGENTS.md` drops it only when every target that reads the file sets it. Codex has two keys that change how rules and permissions are written, `nested-glob-rules` and `exec-policies-from-permissions`. See [Codex config keys](https://agnostic-ai.org/docs/targets/codex-reference/#config-keys). ## `models` Name each model role (a tier) once, then use that name in specs instead of a vendor's model id: ```yaml models: frontier: {claude: fable, codex: astra, effort: xhigh} strong: {claude: opus, codex: sol, effort: high} balanced: {claude: sonnet, codex: terra, effort: medium} fast: {claude: haiku, codex: luna, effort: low} ``` An agent with `model: fast` gets `haiku` in Claude Code and `gpt-6-luna` in Codex, both at `low` effort. Other targets keep their default model. Skills, commands, and settings specs name tiers the same way. Claude Code resolves `opus`, `sonnet`, `haiku`, and `fable` itself. Codex takes only exact ids, so agnostic-ai resolves these aliases when it writes Codex files: | Codex alias | Model id | |-------------|----------| | `sol` | `gpt-6.1-sol` | | `luna` | `gpt-6-luna` | | `astra` | `gpt-6-astra` | | `terra` | `gpt-6.1-terra` | On v0.77.0 or earlier, write `terra` as `gpt-5.6-terra`. Each release pins the model ids. When an upgrade changes one, the next `sync` prints `note: codex: sol now resolves to (was )` once. An alias works anywhere Codex reads a `model`: a tier, `model.codex`, a shared `model`, or a settings spec. `x-codex.model` is written as given. | Tier key | Value | |----------|-------| | A target name | That target's model id. | | `default` | The model id for each target the tier does not name. | | `effort` | One effort for every target, or a per-target map such as `{claude: xhigh, codex: high}`. | - A `model` that names no tier stays a literal model id. A tier with only `effort` keeps each tool's default model. - A tier with neither a model nor `effort` fails to load, and so does any other key (AAI-004, naming the closest target). - Per-spec overrides and which value wins: [model tiers](https://agnostic-ai.org/docs/spec-format/agents/#model-tiers). `explain ` shows the model and effort each target gets, and how an alias resolved. - `lint` warns when a tier a spec names has no entry and no `default` for a target that writes the spec, a tier is named like a Claude model (LINT025), or a tier's `default` is a Claude model another target cannot load (LINT026). - A tier in `agnostic-ai.local.yaml` replaces the same-name tier whole. The [global home config](#global-configuration) accepts the key too. ## `targets` Default: every tool except `amp`, `warp`, `jules`, `goose`, `augment`, `ellipsis`, `grok`, `codebuddy`, and `kimi`. `-t/--target` replaces the list for one run. A likely typo of a built-in target, such as `cursr`, fails before any write. Any other unknown name is skipped with a warning, since it may be an external adapter missing from this machine's PATH. `init` and the first `sync` can write this list through a [picker](https://agnostic-ai.org/docs/cli-reference/sync/#first-sync-target-picker). ## `sync` Per-run flags such as `--diff`, `--format`, and `--jobs` have no config key. See [`sync`](https://agnostic-ai.org/docs/cli-reference/sync/#sync) and [reading a failing `--check`](https://agnostic-ai.org/docs/cli-reference/sync/#reading-a-failing---check). | Key | Default | Effect | |-----|---------|--------| | [`collision-policy`](#synccollision-policy) | `prompt` | What happens when two targets write the same path. | | [`target-overview`](#synctarget-overview) | `false` | Add a section to each entry-point file that lists where generated files live. | | [`resolve-imports`](#syncresolve-imports) | `passthrough` | What tools that cannot resolve `@path` lines get. | | [`dropped-summary`](#syncdropped-summary) | `false` | Print a per-target summary of dropped and downgraded kinds. | | [`shared-skills`](#syncshared-skills) | `false` | Link identical skill folders to one copy. | | [`unmanaged`](#syncunmanaged) | empty | Paths sync never touches. | | [`output-manifest`](#syncoutput-manifest) | `false` | Write `.agnostic-ai/outputs.lock`, the committed list of generated paths. | | [`allow-global-names`](#syncallow-global-names) | empty | Names shared with the global home that `sync` does not warn about. | | [`global-name-clash`](#syncglobal-name-clash) | `warn` | Whether `sync` warns about names shared with the global home. | ### `sync.collision-policy` {#synccollision-policy} Applies when two targets write different content to one path, such as `outputs.codex.rules-file: AGENTS.md` and `outputs.amp.rules-file: AGENTS.md`. Per-target override: `outputs..collision-policy`. | Value | Behavior | |-------|----------| | `prompt` | Default. Fail with an `output collision` error and a hint. | | `prefer-spec` | Write the version of the last tool in `targets:` that writes the file. `sync --check` expects that version. Sync prints one note per winning tool naming the files it won and the tools it overrode; reorder `targets:` to change it. Use in CI when the overlap is intentional. | | `fail` | Hard error with no hint. | ```yaml sync: collision-policy: prefer-spec ``` ### `sync.target-overview` {#synctarget-overview} When `true`, each entry-point file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, ...) gets a section listing where that tool's generated files live, following `outputs..*` overrides. - A shared entry point such as `AGENTS.md` lists each reader in its own section. - Sync rewrites the section between `` and ``. `import` removes it. Aider and external adapters get no section. ### `sync.resolve-imports` {#syncresolve-imports} Sets what a tool that cannot resolve `@path` imports gets for a line in `AGNOSTIC_AI.md` that holds only an `@path` import. `CLAUDE.md` always keeps the line. An `@mention` inside a sentence or a fenced code block is untouched. Paths resolve from the project root. | Value | Tools that cannot resolve it get | |-------|---------------------------| | `passthrough` | Default. The `@`-line as written, as a dead reference. | | `strip` | Nothing: the line is dropped. | | `inline` | The file's content between `` and ``. `import` restores the `@`-line. A missing file fails the sync. | ```yaml sync: resolve-imports: inline ``` ### `sync.dropped-summary` {#syncdropped-summary} When `true`, sync ends with a per-target list of kinds that were dropped (the tool has no support) or downgraded (written only behind an opt-in key, or only in the source directory). It groups capability warnings and [coverage notes](#coverage-notes) by tool. ### `sync.shared-skills` {#syncshared-skills} When `true`, tools that share the Agent Skills layout (`//SKILL.md` plus assets: Claude, Cursor, Codex, Amp) keep one real copy of each skill and link the others to it with relative symlinks. - Only folders with identical content link. Skills with per-tool overrides such as `x-cursor` keep real copies. - Turning the option off restores real copies on the next sync. - Without symlink support (Windows without the privilege), sync warns once and keeps real copies. If you commit shared skill links, Git must check them out as links. Git's `core.symlinks` setting is separate from the system's permission to create links. With `core.symlinks=false`, `sync --check --against index` and `--against HEAD` name each link Git exported as a plain file. To fix it: - Where the system can create links, run `git config --local core.symlinks true` and rerun the check. - To keep `core.symlinks=false`, set `sync.shared-skills: false`, run `agnostic-ai sync`, and stage the config and the restored skill copies. Commit that before you check `--against HEAD`. ### `sync.unmanaged` {#syncunmanaged} Paths you own. `sync` never writes, merges, copies, or removes them. ```yaml sync: unmanaged: - .cursor/rules/legacy.mdc # exact path - .claude/agents/hand-*.md # glob; `*` stays inside one path segment - .claude/skills/legacy/ # trailing slash: everything under the directory ``` - Entries are project-relative with forward slashes. Filename patterns use Go `path.Match`; `**` is not supported. A malformed glob or an empty entry fails config load. - `sync` prints `~ skip (unmanaged) `. `sync --check`, `status`, and `doctor` never count it as drift. `revert` and `doctor --fix` never touch it. - Removing the entry deletes nothing. The next sync rewrites the file. - `sync.shared-skills` never links such a skill folder. - `agnostic-ai.local.yaml` replaces the list whole. - Hook script bodies copied from `.agnostic-ai/scripts/` are not covered. ### `sync.output-manifest` {#syncoutput-manifest} Use it when you commit generated files. A full `sync` writes `.agnostic-ai/outputs.lock`: one line per generated path, with a checksum of what sync wrote. Commit it with the outputs; `sync --check` fails when it is missing or out of date. ```yaml sync: output-manifest: true ``` - With no `.sync-state`, `doctor` and `sync --check` count a tracked file the generated-path list includes but no spec produces as a leftover. `doctor --fix` removes it. - A file edited since sync wrote it no longer matches its checksum, so it is left for you. - `sync --check --against` reads the generated-path list from the state it checks. It catches a deleted spec's outputs even in a shallow clone, unless that list was regenerated in the same commit. ### `sync.allow-global-names` {#syncallow-global-names} Skill and agent names this project shares with the [global home](#global-shared-names) on purpose. `sync` prints no warning for a listed name. Other shared names still warn. ```yaml # agnostic-ai.local.yaml sync: allow-global-names: [gh-issue, gh-issues] ``` - Put the list in `agnostic-ai.local.yaml`, since the home exists only on your machine. A local list replaces the shared one whole. - Names match the way Claude Code folds skill names: case, spacing, and invisible characters do not count. - `doctor` still lists each allowed name under **Global names**, marked as allowed. ### `sync.global-name-clash` {#syncglobal-name-clash} What `sync` does with every name this project shares with the [global home](#global-shared-names): `warn` prints the warning, `ignore` prints nothing. ```yaml # agnostic-ai.local.yaml sync: global-name-clash: ignore ``` - Use it when a project shadows global skills or agents as a rule. To accept a few names, use [`sync.allow-global-names`](#syncallow-global-names). - `doctor` still lists each shared name under **Global names**, marked as ignored. - Any other value fails config load. ## `verify` `verify.command` lists the program to run and its arguments. `agnostic-ai verify` runs it without a shell, so put pipes and redirects in your script. ```yaml verify: command: - ./scripts/verify-harness - --strict ``` See the [`verify` command](https://agnostic-ai.org/docs/cli-reference/check/#verify) for the drift check, input, exit codes, and result recording, which uses flags only. ## `import` ### `import.codex.shred` Sets how `agnostic-ai import codex` treats a nested `AGENTS.md`. The root file always imports into `.agnostic-ai/AGNOSTIC_AI.md`. | Value | Behavior | |-------|----------| | unset | Default. A hand-written file becomes one rule with its full body. A file `sync` wrote splits back into the rules it came from. | | `true` | One rule spec per `##` heading, also for a hand-written file. | | `false` | One rule spec per `AGENTS.md`, with the full body as written. | ```yaml import: codex: shred: false ``` ## `lint` Budgets for the text each tool loads in every session. Past a budget, `agnostic-ai lint` warns and `lint --strict` exits 1. | Key | Default | Effect | |-----|---------|--------| | `instructions-words` | `2000` | Words one target loads every session: entry-point file, always-on rule files, and skill and agent descriptions (LINT011). | | `description-chars` | `1024` | Characters in one skill or agent description (LINT012). A skill description past 1024 still warns under a higher value. | | `codex-chain-bytes` | `32768` | Bytes of `AGENTS.md` Codex reads for one directory: the root file plus each scoped file down to it, review sections included (LINT011). | ```yaml lint: instructions-words: 3000 description-chars: 500 codex-chain-bytes: 24576 ``` The defaults follow vendor limits: Claude Code's [200 lines](https://code.claude.com/docs/en/memory) for `CLAUDE.md`, and the [Agent Skills specification](https://agentskills.io/specification) for descriptions. Lint also warns when text passes Codex's [32 KiB](https://developers.openai.com/codex/guides/agents-md) `AGENTS.md` cap or Antigravity's [24,000-byte](https://antigravity.google/docs/rules) rule cap, whatever the word budget. A missing key or `0` keeps the default. A negative value fails as AAI-004. See [`lint`](https://agnostic-ai.org/docs/cli-reference/check/#lint). The [global home config](#global-configuration) accepts the key too. ## `doctor` ### `doctor.check-references.ignore` {#doctorcheck-referencesignore} Link destinations [`doctor --check-references`](https://agnostic-ai.org/docs/cli-reference/check/#doctor) never reports as broken. ```yaml doctor: check-references: ignore: - url # exact destination, as written in the source Markdown - gcp-url - "*-placeholder" # glob; `*` stays inside one path segment ``` An entry is the destination text as written, such as `url` for `[Logs](url)`, or a `path.Match` glob, as in [`sync.unmanaged`](#syncunmanaged). ## `on-unsupported` Applies when a tool does not support a spec kind, such as `hooks` for Jules or project-level `mcps` for Cline. | Value | Behavior | |-------|----------| | `warn` | Default. Log to stderr and continue. | | `error` | Fail the sync. | | `silent` | Skip without logging. | Any other value fails with AAI-004. It also covers imported Claude hook root references that cannot be translated. See [project-root paths](https://agnostic-ai.org/docs/spec-format/hook-reference/#imported-project-root-paths). ## Coverage notes `sync` prints a `note:` line when a tool does not write specs of a kind you have, because it needs an opt-in key or has no files for them: ``` note: 1 agent reaches warp only via outputs.warp.workflows-dir ``` Setting the named key clears the note. `sync -vv` shows every note. | Target | Kind | Set this to write them | |--------|------|------------------| | `gemini` | skills | `outputs.gemini.emit-skills-as-commands` | | `opencode` | skills | `outputs.opencode.emit-skills-as-commands` | | `warp` | agents | `outputs.warp.workflows-dir` | | `aider` | agents, skills | `outputs.aider.rules-file` | | `zed` | hooks | `outputs.zed.tasks-file` | | `kilo` | agents with `tools` | None. Use `x-kilo: {permission: {...}}`. | | `gemini` | agents with `tools` beyond Read/Write/Edit/Bash/Grep/Glob/WebFetch/WebSearch | None. Use `x-gemini: {tools: [...]}` or drop `tools`. | | `crush` | unknown hook events | Use one of the [13 native lifecycle events](https://agnostic-ai.org/docs/targets/crush/#hooks). Matchers require a tool event. | ## `coverage` `on-unsupported` does not cover coverage notes, and its `error` never fails on them; these keys do. Both are project-only: `sync --global` warns about and ignores a `coverage:` key in the home config. ```yaml coverage: fail-on-notes: true accept: - target: codex kind: agents field: tools reason: Codex limits come from sandbox_mode; tools stays portable for other targets. - target: [codex, gemini, cursor] kind: skills field: argument-hint reason: Only Claude Code shows the hint. ``` ### `coverage.fail-on-notes` {#coveragefail-on-notes} Default `false`. When `true`, every form of `sync` (including `--dry-run`, `--watch`, `--json`, `--check`, and `--plan`) fails on each coverage note that names a target and is not accepted. It prints the notes and rolls back the writes, even under `-q`. ### `coverage.accept` {#coverageaccept} List each note the project accepts and the reason. Each entry names one note with exactly one of `field`, `via`, or `surface`. | Field | Required | Description | |-------|----------|-------------| | `target` | yes | One target name, or a list. A name must be a built-in target or listed in `targets`. | | `kind` | yes | `agents`, `skills`, `rules`, `hooks`, `mcps`, `commands`, `settings`, `reviews`, `environments`, or `ignores`. | | `field` | one of three | Matches "`` on N specs has no effect on ``" notes for that field, whatever the reason. | | `via` | one of three | Matches "N specs reach `` only via ``" notes, and "only in the source dir (``)" notes, with that exact text. | | `surface` | one of three | Matches "N specs reach `` but not ``" notes with that surface. | | `reason` | yes | Why the note is expected. `sync -vv` prints it. | An accepted note no longer prints on `sync`, and `fail-on-notes` ignores it. `sync -vv` shows it as `accepted:` with its reason. `doctor` counts it, and `doctor --json` reports `coverage_accepted`. `lint` warns with LINT024 when an entry matches no note on one of its targets. Config loading fails on an unknown target, a repeated entry, or an entry with no `field`, `via`, or `surface`. ### What coverage.accept cannot match Notes that start with a target name, such as `note: codex: outputs.codex.config.notify is not written`, cover the whole setup. `coverage.accept` cannot match them, and `fail-on-notes` ignores them. A failure from `on-unsupported: error`, such as a Claude model name on another target, is not a note either. ## `gitignore` | Field | Default | Description | |-------|---------|-------------| | `enabled` | `false` when absent; `agnostic-ai init` writes `true` | Every `sync` rewrites a managed `.gitignore` block listing every path the configured adapters write. | | `path` | `.gitignore` | Another file inside the project, such as a nested folder's `.gitignore` or the local-only `.git/info/exclude`. Any other path outside the project or in `.git/` fails to load. | | `commit` | empty | Kinds of generated output to keep in Git, for every target or as `:` for one. The block leaves out their paths. | | `allow` | empty | Gitignore globs written as `!` lines at the end of the block, so a hand-written file at a generated path (such as a `testdata/AGENTS.md` fixture) is not ignored. One pattern per entry; a line break fails to load. | | `worktree-include` | `true` | Keep the same block in `.worktreeinclude`. See [Claude Code worktrees](#claude-code-worktrees). | | `ignore-worktree-include` | `false` | Keep `.worktreeinclude` out of Git. See [Claude Code worktrees](#claude-code-worktrees). | `sync --gitignore` and `init --gitignore` override it per run. See the [CLI reference](https://agnostic-ai.org/docs/cli-reference/start/#init). Use `commit` for files that review bots and a fresh worktree read before any sync runs: ```yaml gitignore: enabled: true commit: [instructions, hooks] ``` - Kinds: `instructions`, `agents`, `skills`, `commands`, `hooks`, `mcps`, `settings`, `reviews`, `environments`, and `ignores`. An unknown kind fails config loading. - `instructions` covers entry-point files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`) and rule outputs, root and scoped. Every other kind is named after its spec source. - A file written by several kinds, such as `.claude/settings.json`, is committed when any of them is listed. - `ellipsis:reviews`, `ellipsis:environments`, and `ellipsis:mcps` also count the settings spec that holds `x-ellipsis`, because `.ellipsis/code-review.yaml` and `.ellipsis/environments/.yaml` need both ([Ellipsis](https://agnostic-ai.org/docs/targets/ellipsis/#keep-the-files-in-git)). - A file that only the config or an [overlay](#watched-inputs) produces belongs to no kind and stays ignored. Prefix a kind with a target to commit it for that target only: ```yaml gitignore: enabled: true commit: [cursor:reviews, cursor:environments] ``` This keeps `.cursor/environment.json`, `.cursor/worktrees.json`, and every `BUGBOT.md` in Git, and ignores `.claude/launch.json` and `.codex/environments/environment.toml`. `lint` warns (LINT017) when the target is not in `targets`. The block sits between `# >>> agnostic-ai (managed) >>>` and `# <<< agnostic-ai (managed) <<<`. Lines outside it are kept. ### Claude Code worktrees A new Claude Code worktree has no gitignored files. Claude Code copies the ones that [`.worktreeinclude`](https://code.claude.com/docs/en/worktrees#copy-gitignored-files-into-worktrees) lists from the main checkout. With `claude` in `targets`, sync also keeps the block in `.worktreeinclude`, minus `.sync-state`, `.command-lock`, Claude worktree directories, and the task lock. Your local files and packs come along, so the worktree renders what the main checkout does. - Set `gitignore.ignore-worktree-include: true` to keep the managed file out of Git by listing it in the managed ignore block. It requires `worktree-include: true` and `claude` in `targets`. If the file is already tracked, run `agnostic-ai sync --untrack` to remove it from the index and keep the file. - Set `gitignore.worktree-include: false` to manage the file yourself. Sync then removes only its own block. A fresh clone or `git worktree` lacks these paths until `sync` runs. See [checkout and merge hooks](https://agnostic-ai.org/docs/git-hooks/#regenerate-on-checkout). ### Block contents - Entries are root-anchored (`/AGENTS.md`), so nested same-named files are not ignored. - Files collapse to their generated subdirectory (`/.claude/rules/`), never higher, so siblings such as `.claude/settings.json` stay visible. A per-kind dir such as `outputs..rules-dir` collapses at the dir itself. - Output under a rule or review `scope` stays one line per file (`/services/api/AGENTS.md`). - The block always holds `agnostic-ai.local.yaml`, `/.agnostic-ai/.command-lock`, `/.agnostic-ai/.sync-state`, `/.agnostic-ai/packs/`, and `/.agnostic-ai/local/`. `init` seeds them even with `gitignore.enabled: false`. - A leftover file from a deleted spec stays ignored until you remove it. - A target can add entries of its own, such as [Claude Code](https://agnostic-ai.org/docs/targets/claude/)'s local settings. - Built-ins come from `agnostic-ai.yaml` alone, so a `builtins` list in `agnostic-ai.local.yaml` leaves the block and `.worktreeinclude` unchanged. Outputs only the local list adds go in an `agnostic-ai (personal)` block in `.git/info/exclude`, and sync removes those lines when the local list stops adding them. Outputs the local list turns off stay in the block. A linked worktree shares `.git/info/exclude` with every checkout, so there sync leaves those outputs untracked and names them in a warning. ## Watched inputs `sync --watch` runs again when the config, sources, local specs, or overlays change. See [watch mode](https://agnostic-ai.org/docs/cli-reference/sync/#watch-mode) for the full list. Overlays in `.agnostic-ai/overlays/` hold keys that specs do not cover, such as Claude `statusLine` or Codex `[history]`. ## Path semantics - Relative `sources` and `outputs` paths start at the directory holding `agnostic-ai.yaml`. Absolute `sources` paths are used as written. - `outputs` paths and `gitignore.path` must stay inside the project and out of `.git/`, so a cloned repository cannot make sync write elsewhere. The config fails to load when one climbs above the project root or points into `.git/`, and sync stops before writing to an absolute `outputs` path outside the project. `gitignore.path` must be relative; `.git/info/exclude` is its one `.git/` exception. - A `sync.unmanaged` entry that climbs above the project root fails to load. A leading `/` anchors an entry at the project root. - Sync skips a `.agnostic-ai/.sync-state` entry outside the project or inside `.git/` and names it in a warning. It never removes or changes that file. - A spec or MCP server named `.git` fails to load. - On Windows, sync refuses to write a file whose name holds a colon, because Windows stores it as a hidden stream of the file named before the colon. Qwen Code and Gemini CLI turn each colon in a command name into a folder, so a command named `git:commit` syncs as `git/commit.md` and `git/commit.toml`, and `import qwen` and `import gemini` read a file in a subfolder back as `git:commit`. A target that writes the name as one file refuses the write, such as Claude Code for commands or Continue for assistants; `sync --check` and `--dry-run` report the same refusal. Name such a spec `git-commit` for those targets. The next sync removes the colon file an older release wrote. Windows can leave an empty file named `git` beside it: Gemini CLI sync removes it to make the `git` folder, and for other targets you can delete it. Gemini CLI sync stops and names a `git` file that holds content, that no colon output in the last sync explains, or whose old colon file you edited; copy any text you need, delete `git`, and sync again. - On Windows, import refuses an MCP server or other spec whose file name would hold a colon, such as a server named `ns:tool`, and names the file. `import --global` skips such a server with a warning that names the home config file. Rename the server before you import it; Continue needs the rename too, since sync writes `.continue/mcpServers/.yaml`. An older release reported such an import as done and may have left an empty file such as `.agnostic-ai/mcps/ns`, which you can delete. - Project sync refuses to write or remove through a link that leads outside the project or into a `.git` folder, and names the link. A link that stays inside the project, such as `CLAUDE.md` linked to `AGENTS.md`, still works. `sync --global` follows linked dotfiles as before. A linked `.agnostic-ai` folder keeps the sync lock and ledger only when it holds `AGNOSTIC_AI.md`. - Sync creates output directories as needed and overwrites existing files. ## Entry-point files `sync` copies `.agnostic-ai/AGNOSTIC_AI.md` into one root entry-point file per enabled tool. See the [per-target table](https://agnostic-ai.org/docs/target-behavior/#entry-point-files). `.agnostic-ai/local/AGNOSTIC_AI.md` [extends that body](https://agnostic-ai.org/docs/local-overrides/#extend-the-instructions) on one machine. Write the instructions every tool shares in `AGNOSTIC_AI.md`. Every session loads this text, so keep it to what an agent cannot infer from the code. Rules, agents, and skills stay in their own `sources` folders. Sync never overwrites this file; it overwrites only the root entry points. When the file is missing, sync creates it with one line saying the tool files are generated from `.agnostic-ai/`. A project from an earlier release can still hold the old, long default text; `doctor` points it out. Replace it with your own instructions. ### Legacy merged rules file Setting `outputs..rules-file: ` restores the legacy layout: the adapter writes one merged document at ``, including the built-in skill instructions. Two adapters writing different content to one path fail unless you set `sync.collision-policy: prefer-spec`. When `` is the target's own entry-point file (`outputs.claude.rules-file: CLAUDE.md`), sync skips the pointer body. Any other path keeps the entry-point file, and for `claude` the pointer body also gains an `@` import. ### Per-target paragraphs `.agnostic-ai/AGNOSTIC_AI.md` accepts the `::target` / `::targets` / `::end` fences from [spec bodies](https://agnostic-ai.org/docs/spec-format/#per-target-body-fences), with the same rules for shared entry-point files. - `validate` flags a fence naming an unknown target or one that reads no entry-point file. - For how `import` treats a fenced source, see [import](https://agnostic-ai.org/docs/cli-reference/start/#files-and-source-order). ## Which value wins {#precedence} Last wins: 1. Built-in defaults 2. `agnostic-ai.yaml` 3. `agnostic-ai.local.yaml` 4. CLI flags (such as `agnostic-ai sync -t claude`) ## Layered specs Specs load from four layers, lowest first. A higher layer overrides a spec of the same kind and name; a new name adds a spec. The `project-user` layer merges into the shared spec field by field (see [local overrides](https://agnostic-ai.org/docs/local-overrides/#override-fields)). `agnostic-ai list` shows each spec's layer. | Layer | Root | Loaded when | |-------|------|-------------| | `builtin` | bundled specs | enabled with `builtins` | | packs | `.agnostic-ai/packs/` from `agnostic.packs.lock` | packs are installed | | `project` | `agnostic-ai.yaml` `sources` paths | always | | `project-user` | `/.agnostic-ai/local` | directory exists | Only `project` uses custom `sources` paths. `$AGNOSTIC_AI_HOME` is not a project layer. ## Global configuration `agnostic-ai sync --global` installs your own instructions, skills, hooks, settings, agents, and MCP servers for every project. It reads `~/.agnostic-ai/`, not the current project's specs. [Global configuration](https://agnostic-ai.org/docs/global-configuration/) covers the source layout, target selection, ownership, and recovery. ```bash agnostic-ai sync --global --only claude,codex --dry-run agnostic-ai sync --global --only claude,codex agnostic-ai sync --global --only claude,codex --check ``` ### Shared names {#global-shared-names} A home skill can hide a project skill with the same name. See [shared names](https://agnostic-ai.org/docs/global-configuration/#global-shared-names). ### Default model and effort {#global-default-model-and-effort} Home settings specs set user defaults. See [user settings](https://agnostic-ai.org/docs/global-configuration/#global-default-model-and-effort). ### MCP servers {#global-mcp-servers} See [user MCP servers](https://agnostic-ai.org/docs/global-configuration/#global-mcp-servers). --- # Global configuration Keep personal instructions, rules, hooks, skills, agents, settings, and MCP servers in one home folder, and run `agnostic-ai sync --global` to install them into each tool's user-level files, so they apply in every project. Use it for what you want everywhere; put team defaults in a project or a pack. ## Source and targets The source folder is `$AGNOSTIC_AI_HOME`, or `~/.agnostic-ai/` when unset. `sync --global` runs from any directory and loads no packs or project specs. [Global output](#global-output) lists the paths it writes. ```text ~/.agnostic-ai/ ├── agnostic-ai.yaml # optional: targets, builtins, requires, lint, on-unsupported, models ├── AGNOSTIC_AI.md ├── agents/*.md ├── rules/*.md ├── hooks/*.yaml ├── settings/*.yaml # default model and effort ├── mcps/*.yaml # user-level MCP servers ├── skills//SKILL.md └── local/ # optional personal layer, same layout ``` `local/` works as in a project: a spec there merges into the shared spec of the same kind and name, and a new name adds one. See [local overrides](https://agnostic-ai.org/docs/local-overrides/#override-fields). `local/AGNOSTIC_AI.md` comes last in the managed instructions block. Add `/local/` to the source root's `.gitignore` before adding personal files. To sync a fixed set of tools without `--only` on every run, list them in the source root's `agnostic-ai.yaml`: ```yaml targets: [claude, codex, cursor] ``` - `sync --global`, `lint --global`, and `validate --global` then use only those targets. A `targets` list in `local/agnostic-ai.yaml` replaces the shared one. - `--only` and `--except` narrow the list for one run and must name configured targets. `--target` replaces it and skips the home config's `targets`. - A target with no user-level files, such as `aider` or `continue`, is skipped with one warning. An unknown name stops the run. - [`builtins`](https://agnostic-ai.org/docs/configuration/#built-ins), [`requires`](https://agnostic-ai.org/docs/configuration/#requires), `lint`, and [`models`](https://agnostic-ai.org/docs/configuration/#models) work as in a project. For each key, `local/agnostic-ai.yaml` replaces the shared value, and a `models` tier replaces the same-name tier whole. - When a file's tiers do not load, `sync --global` stops. With `-t`, it warns, skips that file's tiers, and omits the `model` of any spec that names one. - [`on-unsupported`](https://agnostic-ai.org/docs/configuration/#on-unsupported) (`warn`, `error`, or `silent`) sets what `sync --global` does with a skill line that Claude Code expands and other targets read as plain text. See [Claude Code body syntax](https://agnostic-ai.org/docs/spec-format/skills/#claude-code-body-syntax). - Other keys except `version` print a warning and are ignored. - A target dropped from the list keeps its synced files and ownership records until you remove them by hand. `list --global` shows the specs in effect with their `builtin`, `global`, or `global-local` layer. `validate --global` and `lint --global` check before a sync writes. `migrate --global` rewrites old spec forms. The [CLI reference](https://agnostic-ai.org/docs/cli-reference/sync/#sync) lists the accepted `sync` flags. - Nested rules, rules with scope, path, glob, or target conditions, commands, settings `permissions` rule lists for targets other than Claude Code and CodeBuddy Code, inheritance, and merging with project specs are unsupported. - Kilo inline shell placeholders pass the syntax check at documented trusted user destinations. See [Kilo shell commands](https://agnostic-ai.org/docs/targets/kilo/#shell-commands-in-global-skills) for approval, scope, and the kill switch. - Skills copy their supporting files unchanged. Shared directories such as `~/.agents/skills/` keep neutral frontmatter. See [skill invocation support](https://agnostic-ai.org/docs/spec-format/skills/#disable-model-invocation-support-by-target) for `disable-model-invocation`, including OpenCode V2. - Hooks, skills, and settings honor `target`, `targets`, and `targets-exclude`. A settings spec routed away from a tool sets none of its values there and does not override an earlier spec. Supported portable `on` events and `match` values translate for each tool. Native `event` and `matcher` values pass through unchanged; set those per target. See [portable hooks](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). - Some user hook routes have their own limits. Copilot: `import --global copilot` adopts matching handlers from neighboring user JSON files; see [Copilot hooks](https://agnostic-ai.org/docs/targets/copilot/#hooks). Cline: the VS Code extension does not read the hooks sync writes; see [Cline global hooks](https://agnostic-ai.org/docs/targets/cline/#global-hooks). Kiro CLI V3: one `~/.kiro/hooks/.json` per spec, in the project hook format, with shared scripts in `~/.kiro/scripts/`; unrelated hook files stay, and a conflicting file stops sync. - Targets without global agent output warn and skip agents. - Output is real files, never symlinks. A user file that is itself a symlink, such as a dotfiles-managed `CLAUDE.md`, is written through. A symlink inside a skills, agents, or rules directory stops the run. - Sync records what it wrote in `$AGNOSTIC_AI_HOME/state/global.json` and removes only those files and settings for the targets in the run. With `--only`, other targets' output stays in place. When the record is missing or lacks newly imported specs, `--check` notes it and fails only when a tool file differs too. - A hand edit to a file sync owns, or to an instructions file's managed block, stops the run and names the file. Move the edit into the source, or rerun with `--backup` to overwrite or remove it and keep `.bak`. Text outside the managed block never counts. - A file or hook that already matches what sync would write is adopted. The run stops on one that differs by even one byte, an unmanaged agent, skill, or rule collision, a damaged marker, invalid native JSON, corrupt state, or state recorded under another `HOME`. - Without `--only`, explicit targets, or a home `targets` list, a target with a relative root variable or an invalid agent name is skipped with a warning. Naming it makes that an error. - Sync Goose and OpenHands together to update their shared agent file. Global specs never merge with project specs. When both exist, each tool decides which settings or same-named specs win; see [shared names](#global-shared-names). Ordinary `agnostic-ai sync` reads only the names of `~/.agnostic-ai/` specs, for that warning. Inside the global source root, `sync`, `init`, `import`, `new`, `packs`, `cleanup`, `revert`, and `install-hook` stop before any write and point you to the `--global` form. Read-only commands such as `lint`, `validate`, and `doctor` still run. For a home kept in Git, `install-hook --global` writes a pre-commit hook that runs `lint --global --strict`, `validate --global`, and `sync --global --check`. To start a home from what your tools already hold, run `agnostic-ai import --global`. See [import](https://agnostic-ai.org/docs/cli-reference/start/#import). `import --global` reads [voice dictation](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) keys into shared `settings/imported.yaml`. Tool arguments select which native files to read. Existing home voice specs keep their scope; see [voice import](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) for exclusions and skipped values. ### Shared names {#global-shared-names} A project skill or agent can share its `name` with one in the home. Sync writes both, and each tool decides which one it loads. `sync` and `doctor` print one warning per shared name, with each target where one copy hides the other, which copy wins, and the fix: ```text ! .agnostic-ai/skills/gh-issue/SKILL.md: skill "gh-issue" also exists in ~/.agnostic-ai/skills/gh-issue/SKILL.md with different content; claude loads the global one, which exists only on this machine, gemini loads this one; to load both, rename the global one (such as gh-issue-personal), or delete it to drop it, then run `agnostic-ai sync --global`; project sync cannot select this skill instead on claude ``` Claude Code and Amp document no project setting that picks the project copy over a home skill and keeps the other home skills; Amp's `amp.skills.disableGlobalAgentsSkills` turns off every skill in the global agents folders. So sync keeps both copies, explains the limit, and leaves your home files alone. Doctor reports it even when sync warnings are off. A winning global copy changes what you run, not what others run from the repository. When only project copies win, the warning says the global one is unused here; when both copies match, it says to delete one before they drift apart. The check covers these targets: | Target | Skill with the same name | Agent with the same name | |--------|--------------------------|--------------------------| | Amp | Global wins when both sync `amp`: `~/.agents/skills/` masks `.agents/skills/` ([skills](https://ampcode.com/docs/customize/skills)) | No global agents | | Claude Code | Global wins ([skills](https://code.claude.com/docs/en/skills)) | Project wins ([subagents](https://code.claude.com/docs/en/sub-agents)) | | Codex | Both can appear in skill selectors, so no warning ([skills](https://learn.chatgpt.com/docs/build-skills)) | Not checked | | Gemini CLI | Project wins ([skills](https://geminicli.com/docs/cli/skills/)) | Not checked | Sync and doctor do not check other tools, though some document what happens: - [Kiro](https://kiro.dev/docs/skills/#skill-scope) prefers workspace skills over global skills. - [Qoder](https://docs.qoder.com/extensions/skills) prefers project skills over user skills. - [Warp](https://docs.warp.dev/agents/capabilities/skills#skill-name-conflicts) prefers home skills, then higher directories, when it resolves names in the background. Its slash-command menu shows all matches, and a plain-language request gives the agent every match in scope with its path. ### Which pairs the check compares - Targets that both the project and the home's [`targets`](#source-and-targets) write. A spec whose `target`, `targets`, or `targets-exclude` leaves one of them out is skipped. - Claude Code skill names match ignoring case, spacing, invisible characters, and fullwidth forms. Other targets compare names exactly. - A spec shared through a link is not a clash. Neither is a project that is the home. To layer on purpose, give the project spec its own name, such as a `gh-issue-project` skill beside a global `gh-issue`. Both then load everywhere, under different names. To keep one name in both places, list it in [`sync.allow-global-names`](https://agnostic-ai.org/docs/configuration/#syncallow-global-names). To stop the warning for every shared name, set [`sync.global-name-clash: ignore`](https://agnostic-ai.org/docs/configuration/#syncglobal-name-clash). ### Default model and effort {#global-default-model-and-effort} A settings spec in the home sets each tool's default model and effort in its user settings file. `model` and `effort` each take one string for every target, or a per-target map with an optional `default`: ```yaml # ~/.agnostic-ai/settings/defaults.yaml model: claude: opus codex: gpt-6-luna effort: claude: high codex: high ``` | Target | File | Keys | |---|---|---| | codex | `~/.codex/config.toml` | `model`, `model_reasoning_effort` | | claude | `~/.claude/settings.json` | `model`, `effortLevel`, `permissions.defaultMode`, `permissions.allow`, `ask`, `deny` | | codebuddy | `~/.codebuddy/settings.json` | `model`, `reasoningEffort`, `permissions.defaultMode`, `permissions.allow`, `ask`, `deny` | | copilot | `~/.copilot/settings.json` | `model`, `effortLevel` | | qoder | `~/.qoder/settings.json` | `model.name`, `model.reasoningEffort` | | vibe | `~/.vibe/config.toml` | `active_model` | | kimi | `~/.kimi-code/config.toml` | `default_model`, `default_plan_mode`, `telemetry` | | gemini | `~/.gemini/settings.json` | `model.name` | Targets not in the table raise a coverage note for `model` and `effort`, and so does `effort` on Gemini and Kimi Code. Augment has no default model or effort key but takes `x-augment` keys, such as `shell`. Except for Kiro CLI's literal dotted keys, a dotted key is a nested JSON object: for `model.name`, sync sets `name` inside `model` and leaves the other keys alone. Accepted `effort` values: Claude and Copilot take `low`, `medium`, `high`, `xhigh`; Qoder takes `disabled`, `off`, `none`, `low`, `medium`, `high`, `xhigh`, `max`; Codex takes any string; for CodeBuddy Code see [native fields and limits](https://agnostic-ai.org/docs/targets/codebuddy/#fields-and-limits). `lint --global` (LINT014) and `validate --global` flag a value a target cannot take. Claude Code reads `effortLevel` in `~/.claude/settings.json` on Opus 5, Fable 5.1, and earlier models only. Opus 5.5 and later models ignore it and need a level saved per model with `/effort`. `sync --global` writes the key and prints a note. In project settings, `effortLevel` applies to every model. Set Claude Code's starting permission mode in a home settings spec: ```yaml # ~/.agnostic-ai/settings/defaults.yaml permissions: default-mode: acceptEdits ``` `sync --global --only claude` writes `permissions.defaultMode` in `~/.claude/settings.json`, or under `CLAUDE_CONFIG_DIR`. It accepts `default`, `manual`, `acceptEdits`, `plan`, `auto`, `dontAsk`, and `bypassPermissions`. A later settings spec wins. Removing the field removes the managed mode, unless you changed it by hand. Claude's `auto` and `bypassPermissions` need user, managed, or session settings; see [Claude's mode reference](https://code.claude.com/docs/en/settings-reference#permissions-defaultmode). Home `permissions.allow`, `ask`, and `deny` rules, and the same lists under `x-claude.permissions`, merge into `~/.claude/settings.json` rule by rule (v0.86.0 or later): - Sync appends each missing rule after the rules already there, and adopts a rule already there. - Sync records added and adopted rules in `state/global.json`. Dropping a rule from the specs removes it, including a rule you wrote before adoption. Other rules keep their order. - `--check` fails while a spec rule is missing or a dropped one remains. `--plan` and `--dry-run` list each rule added or removed. - Run `import --global claude` to copy user-level rules into `settings/permissions-claude.yaml`, so they reach your other machines. Claude Code saves standing permission approvals to the project's `.claude/settings.local.json`, which global import does not read. See [Claude's settings scopes](https://code.claude.com/docs/en/settings#find-or-create-your-settings-files). CodeBuddy Code also merges user permission lists and starting modes; see its [native limits](https://agnostic-ai.org/docs/targets/codebuddy/#fields-and-limits). Any `permissions` field on another target raises a coverage note. For Codex, set `x-codex.approval_policy` and `x-codex.sandbox_mode` instead. `session-retention-days` in a home settings spec sets how many days your transcripts are kept, for Claude Code, CodeBuddy Code, Gemini CLI, and Qoder CLI. See [session retention](https://agnostic-ai.org/docs/spec-format/settings/#session-retention). `voice` in a home settings spec configures voice dictation for tools with a native setting. See [voice dictation](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation). Each item wins over the one before it: 1. `settings/*.yaml` in the home. 2. `local/settings/*.yaml`. A same-named file merges into the shared one field by field. A new file comes after the shared ones, so its `model` and `effort` win. 3. The project tier: a project's settings file, such as `.codex/config.toml` or `.claude/settings.json`, wins according to the tool's own settings order. 4. An agent's own `model` and `effort`, for that agent. 5. The tool's flag for one run, such as `codex -m` or `claude --model`. Sync edits only the keys it writes. Removing the spec removes only those keys and the objects sync created to hold them, so an object you wrote, even an empty one such as `"env": {}`, stays. Sync deletes a file it created once the file is empty. - A key that already holds the value sync would write is adopted. A key with another value stops the run and names the file, key, and both values; this is normal for Codex, whose `/model` picker saves to `config.toml`. `--backup` overwrites the key and keeps `.bak`. - `--dry-run` lists each key a write sets or removes, and `--check` fails on a changed key. - An `x-` block sets that target's own keys in the same file. Nested objects merge leaf by leaf. An `x-claude` key wins over the portable field it shares a key with. A later spec wins key by key, and `null` drops an earlier key. - Global sync writes Codex top-level scalars and arrays, such as `x-codex.notify`, to `~/.codex/config.toml`. Tables such as `profiles`, `x-claude.hooks`, and `x-claude.permissions` keys other than `allow`, `ask`, and `deny` raise a coverage note. Project sync does not route `x-codex` into `.codex/config.toml`. - `agnostic-ai explain --global settings/defaults.yaml` names the file and key each target gets. ### Agents {#global-agents} A personal agent such as `~/.agnostic-ai/agents/reviewer.md` with `targets: [claude, codex]` installs with: ```console agnostic-ai sync --global --only claude,codex agnostic-ai sync --global --only claude,codex --check ``` Sync writes `~/.claude/agents/reviewer.md` and `~/.codex/agents/reviewer.toml`. If a hand-copied file sits at an output path, copy its edits into the source and move the file aside first; `--backup` does not override a file sync does not own. ### MCP servers {#global-mcp-servers} MCP specs in the home's `mcps/` install each server in the user MCP file of every target that has one, rendered as the target's project MCP file renders it: | Target | File | Where | |---|---|---| | claude | `~/.claude.json` (`$CLAUDE_CONFIG_DIR/.claude.json` when set) | top-level `mcpServers.` | | codex | `~/.codex/config.toml` | `[mcp_servers.]` table | | codebuddy | `~/.codebuddy/.mcp.json` | `mcpServers.`; [legacy precedence and native limits](https://agnostic-ai.org/docs/targets/codebuddy/#enable-project-mcp-output) | | vibe | `~/.vibe/config.toml` | `[[mcp_servers]]`, keyed by `name` | | kimi | `~/.kimi-code/mcp.json` | `mcpServers.`, retaining disabled servers | | cursor | `~/.cursor/mcp.json` | `mcpServers.` | | copilot | `~/.copilot/mcp-config.json` | `mcpServers.`, with `tools: ["*"]` when the spec sets none | | gemini | `~/.gemini/settings.json` | `mcpServers.` | | qoder | `~/.qoder/settings.json` | `mcpServers.` | | warp | `~/.warp/.mcp.json` (Warp app only; [the Agent CLI reads its own file](https://agnostic-ai.org/docs/targets/warp/#app-and-cli)) | `mcpServers.`: `{command, args, env, working_directory}` or `{url, headers}` | | augment | `~/.augment/settings.json` | `mcpServers.` | | antigravity | `~/.gemini/config/mcp_config.json` | `mcpServers.`: `{command, args, env, cwd}` or `{serverUrl, headers}`, with optional `disabled` | `serve --global` lists, on its **Tools** page, the servers in these files that no home spec covers and sync did not write, with the `import --global ` command that makes each one a spec. See [serve](https://agnostic-ai.org/docs/cli-reference/inspect/#serve). OpenHands has no user MCP file sync can write. Sync prints a note that points at Agent Canvas **Customize > MCP Servers**, and removes the servers an earlier sync recorded as its own in `~/.openhands/mcp.json`. See [OpenHands MCP](https://agnostic-ai.org/docs/targets/openhands/#mcp). Each server follows the per-key rules above: - A hand-written server that means the same as the spec is adopted. A different one with the same name stops the run, and `--backup` overwrites it. - A server sync wrote is removed when its spec goes. Servers you add under other names stay. - In `~/.claude.json`, sync edits only its own `mcpServers` entries and creates a missing file at `0600`. If the file changed after sync read it, sync stops without writing; run it again. - A spec with `disabled: true` stays out of the Augment, Claude, Cursor, Copilot, and Warp user files, where a listed server is live in every project. Antigravity keeps disabled servers with `disabled: true` in its user file. - When `CLAUDE_CONFIG_DIR` or another root variable moves a file, the next sync removes its entries from the old one. ## Global output A dash means global sync writes nothing for that kind. | Target | Instructions | Rules | Hooks | Skills | Agents | Session retention | |--------|--------------|-------|-------|--------|--------|-------------------| | **claude** | `~/.claude/CLAUDE.md` | inlined | `~/.claude/settings.json` | `~/.claude/skills//` | `~/.claude/agents/.md` | [days](https://agnostic-ai.org/docs/spec-format/settings/#session-retention) | | **codebuddy** | `~/.codebuddy/CODEBUDDY.md` | inlined | `~/.codebuddy/settings.json` | `~/.codebuddy/skills//` | `~/.codebuddy/agents/.md` | [days](https://agnostic-ai.org/docs/targets/codebuddy/#fields-and-limits) | | **cursor** | `~/.cursor/AGENTS.md` (bridged) | inlined | `~/.cursor/hooks.json` | `~/.cursor/skills//` | `~/.cursor/agents/.md` | - | | **codex** | `~/.codex/AGENTS.md` | inlined | `~/.codex/hooks.json` | `~/.agents/skills//` | `~/.codex/agents/.toml` | - | | **gemini** | `~/.gemini/GEMINI.md` | inlined | `~/.gemini/settings.json` | `~/.gemini/skills//` | `~/.gemini/agents/.md` | [days](https://agnostic-ai.org/docs/spec-format/settings/#session-retention) | | **kimi** | `~/.kimi-code/AGENTS.md` | inlined | `~/.kimi-code/config.toml` | `~/.kimi-code/skills//` | `~/.kimi-code/agents/.md` | - | | **vibe** | `~/.vibe/AGENTS.md` | inlined | `~/.vibe/hooks.toml` | `~/.vibe/skills//` | `~/.vibe/agents/.toml` | - | | **qoder** | `~/.qoder/AGENTS.md` | inlined | `~/.qoder/settings.json` | `~/.qoder/skills//` | `~/.qoder/agents/.md` | [days](https://agnostic-ai.org/docs/spec-format/settings/#session-retention) | | **copilot** | `~/.copilot/copilot-instructions.md` | inlined | `~/.copilot/hooks/agnostic-ai.json` | `~/.copilot/skills//` | `~/.copilot/agents/.agent.md` | - | | **cline** | `~/.agents/AGENTS.md` | inlined | `~/.cline/hooks/` | `~/.cline/skills//` | `~/.cline/agents/.yml` | - | | **windsurf** | `~/.config/devin/AGENTS.md` | inlined | - | `~/.agents/skills//` | `~/.config/devin/agents/.md` | - | | **amp** | `~/.config/amp/AGENTS.md` | inlined | - | `~/.agents/skills//` | - | - | | **zed** | `~/.config/zed/AGENTS.md`; Windows: `%APPDATA%\Zed\AGENTS.md` | inlined | - | `~/.agents/skills//` | - | - | | **warp** | `~/.agents/AGENTS.md` | inlined | - | `~/.agents/skills//` | - | - | | **opencode** | `~/.config/opencode/AGENTS.md` | inlined | - | `~/.config/opencode/skills//` | `~/.config/opencode/agents/.md` | - | | **antigravity** | `~/.gemini/GEMINI.md` | inlined | - | `~/.gemini/config/skills//` | `~/.gemini/config/agents//agent.md` | - | | **junie** | `~/.junie/AGENTS.md` | inlined | - | `~/.junie/skills//` | `~/.junie/agents/.md` | - | | **kiro** | `~/.kiro/steering/AGENTS.md` | inlined | `~/.kiro/hooks/.json` | `~/.kiro/skills//` | `~/.kiro/agents/.md` | - | | **crush** | `~/.config/crush/CRUSH.md` | inlined | - | `~/.config/crush/skills//` | - | - | | **factory** | `~/.factory/AGENTS.md` | inlined | - | `~/.factory/skills//` | `~/.factory/droids/.md` | - | | **kilo** | `~/.config/kilo/AGENTS.md` | inlined | - | `~/.kilo/skills//` | `~/.config/kilo/agents/.md` | - | | **goose** | `~/.config/goose/.goosehints` | inlined | - | `~/.agents/skills//` | `~/.agents/agents/.md` | - | | **openhands** | - | - | - | `~/.agents/skills//` | `~/.agents/agents/.md` | - | | **trae** | - | - | - | `~/.trae/skills//` | `~/.trae-cn/agents/.md` | - | | **grok** | `~/.grok/AGENTS.md` | inlined | `~/.grok/hooks/.json` | `~/.grok/skills//` | `~/.grok/agents/.md` | - | | **qwen** | `~/.qwen/QWEN.md` | inlined | `~/.qwen/settings.json` | `~/.qwen/skills//` | `~/.qwen/agents/.md` | - | | **augment** | - | `~/.augment/rules/.md` | `~/.augment/settings.json` | `~/.augment/skills//` | `~/.augment/agents/.md` | - | - [MCP servers](#global-mcp-servers) and [settings](#global-default-model-and-effort) have their own tables above. Agents use the same formats as project agents; see [the agent example](#global-agents). - **Rules** are inlined into the instructions file, in the same `## Rules` block as the shared body. Augment has no user-level instructions file for the CLI, so its `~/.augment/rules/` entries are always on. - Existing instruction files keep their personal text and file permissions. Sync stops if a file changes while it is preparing the update. Paths marked `~/.config/` follow `XDG_CONFIG_HOME` when set, except Zed on macOS. Zed on Linux and FreeBSD prefers `FLATPAK_XDG_CONFIG_HOME`; see [its native instruction roots](https://agnostic-ai.org/docs/targets/zed/#global-instructions). Devin agents use `~/.config/devin/agents/` on Linux and macOS and `%APPDATA%\devin\agents\` on Windows. ### Native discovery and shared paths - **Discovery**: Copilot's path is for its CLI. OpenHands discovery applies to local conversations. Devin custom profiles are experimental. Trae needs **Settings > Beta > Subagents > Enable Subagents Directory**. - **Configuration roots**: `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `COPILOT_HOME`, `CLINE_DIR`, `QODER_CONFIG_DIR`, `KIRO_HOME`, `JUNIE_HOME`, and `KIMI_CODE_HOME` replace the matching `~/./` folder, and `GEMINI_CLI_HOME` is the parent of `.gemini/`. Everything under that folder moves together; paths outside it, such as `~/.agents/skills/`, stay put. Use absolute paths. - **Shared paths** are written once: `~/.agents/skills/` (codex, windsurf, amp, zed, warp, goose, openhands), `~/.agents/agents/` (goose, openhands), `~/.agents/AGENTS.md` (cline, warp), `~/.gemini/GEMINI.md` (gemini, antigravity). If two tools would write different content to one path, the run stops before writing and names the path. - **Copilot agent effort**: Copilot agent profiles have no effort key. A portable agent `effort` of `low`, `medium`, `high`, or `xhigh` goes to `subagents.agents..effortLevel` in `~/.copilot/settings.json`; other values raise a coverage note. Sync stops when the file has comments, since a rewrite drops them (rerun with `--backup` to rewrite it and keep `settings.json.bak`), or when a hand-set `effortLevel` has a different value. - **Cursor** does not load the home-level `AGENTS.md`. Global sync installs a `sessionStart` hook and a script under `~/.cursor/hooks/` (POSIX shell on macOS and Linux, PowerShell on Windows) that passes the instructions to Cursor. It needs neither agnostic-ai, Python, nor jq, adds context without enforcing policy, and keeps existing `sessionStart` entries. - **OpenCode** keeps `disable-model-invocation` in project and global skills. V2 honors it; V1 does not. See [OpenCode skills](https://agnostic-ai.org/docs/targets/opencode/) for native metadata precedence. - **OpenCode V1** reads `~/.claude/CLAUDE.md` only when `~/.config/opencode/AGENTS.md` does not exist, so syncing both tools stops Claude-only text from reaching it. [V2 discovers only `AGENTS.md`](https://opencode.ai/v2/docs/migrate-v1#instruction-files). Move any guidance from the Claude fallback into OpenCode's `AGENTS.md`. - **Devin CLI and VS Code Copilot** read `~/.claude/` by default, so syncing claude plus windsurf or copilot delivers the same instructions through two paths. ### Targets left out - Aider reaches a home instructions file only through a `read:` entry in `~/.aider.conf.yml`. - Continue's one documented home setting is the `rules:` list in the `config.yaml` it rewrites itself. - Jules documents nothing at user level. - Ellipsis reads only files in the repository. Hooks reach these tools at user level: Claude Code, CodeBuddy Code, Codex, Gemini, Qoder, Cursor, Augment, Kiro CLI V3, Copilot, Cline SDK/CLI, Grok Build, Kimi Code CLI, Qwen Code, and Mistral Vibe. The other tools have no user-level hook output: - Factory, Devin CLI, Antigravity, Goose, and Crush use hook formats that global sync does not write. - Junie's hooks are Early Access. - Amp, OpenCode, and Kilo take hooks only as TypeScript plugins. Sync writes them for OpenCode (`.opencode/plugins/`) and Kilo (`.kilo/plugin/`) at project level only. OpenCode defaults to V1; a hook selects [V2](https://agnostic-ai.org/docs/targets/opencode/#opencode-v2-hooks) with `x-opencode.plugin-api: v2` on macOS and Linux. On Windows, V2 refuses matching callbacks, so keep the default V1 hooks. --- # CLI reference ``` agnostic-ai [command] [flags] ``` Run project commands from the folder containing `agnostic-ai.yaml`, unless the command documents support for subdirectories or `--global`. Use `agnostic-ai --help` for the options available in your installed release. To troubleshoot, start with [`doctor`](https://agnostic-ai.org/docs/cli-reference/check/#doctor) and follow its next step, or see [troubleshooting](https://agnostic-ai.org/docs/troubleshooting/) by symptom. ## Find a command Root help groups commands into setup, daily generation, diagnosis and inspection, and maintenance. A fresh project can follow this path: ```bash echo "claude,codex" | agnostic-ai init agnostic-ai sync --plan agnostic-ai sync agnostic-ai doctor ``` | Task | Commands | |---|---| | Set up a project | [use](https://agnostic-ai.org/docs/cli-reference/start/#use), [init](https://agnostic-ai.org/docs/cli-reference/start/#init), [import](https://agnostic-ai.org/docs/cli-reference/start/#import), [new](https://agnostic-ai.org/docs/cli-reference/start/#new) | | Generate or preview output | [sync](https://agnostic-ai.org/docs/cli-reference/sync/#sync), [render](https://agnostic-ai.org/docs/cli-reference/inspect/#render) | | Diagnose problems, then check a specific area | [doctor](https://agnostic-ai.org/docs/cli-reference/check/#doctor), [validate](https://agnostic-ai.org/docs/cli-reference/check/#validate), [lint](https://agnostic-ai.org/docs/cli-reference/check/#lint), [status](https://agnostic-ai.org/docs/cli-reference/check/#status), [verify](https://agnostic-ai.org/docs/cli-reference/check/#verify) | | Find which specs each tool reads | [list](https://agnostic-ai.org/docs/cli-reference/start/#list), [explain](https://agnostic-ai.org/docs/cli-reference/inspect/#explain), [compare](https://agnostic-ai.org/docs/cli-reference/inspect/#compare), [graph](https://agnostic-ai.org/docs/cli-reference/inspect/#graph), [why](https://agnostic-ai.org/docs/cli-reference/inspect/#why) | | Inspect tool permissions and settings | [ui / view](https://agnostic-ai.org/docs/cli-reference/inspect/#view), [serve](https://agnostic-ai.org/docs/cli-reference/inspect/#serve) | | List each MCP server's tools for the overview | [mcp tools](https://agnostic-ai.org/docs/cli-reference/inspect/#mcp-tools) | | Restore or remove generated files | [revert](https://agnostic-ai.org/docs/cli-reference/maintain/#revert), [cleanup](https://agnostic-ai.org/docs/cli-reference/maintain/#cleanup) | | Share specs | [packs](https://agnostic-ai.org/docs/cli-reference/maintain/#packs) | | Check and maintain shared memory | [memory commands](https://agnostic-ai.org/docs/cli-reference/memory/#memory) | | Write and test hook commands | [hook paths](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-paths), [hook run](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run), [hook worktree-remove](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-worktree-remove) | | Check or prepare the project CLI | [project](https://agnostic-ai.org/docs/cli-reference/project/#project), [readiness checks](https://agnostic-ai.org/docs/cli-reference/project/#project-readiness) | | Set up your environment | [completion](https://agnostic-ai.org/docs/cli-reference/maintain/#completion), [upgrade or update](https://agnostic-ai.org/docs/cli-reference/upgrades/#upgrade), [migrate](https://agnostic-ai.org/docs/cli-reference/upgrades/#migrate), [install-hook](https://agnostic-ai.org/docs/cli-reference/maintain/#install-hook), [lsp](https://agnostic-ai.org/docs/cli-reference/maintain/#lsp) | Walkthroughs: [Getting started](https://agnostic-ai.org/docs/getting-started/), [Migration](https://agnostic-ai.org/docs/migration/). Automation: [exit codes](#exit-codes), [CI guide](https://agnostic-ai.org/docs/ci/). ## Concurrent commands `sync` (including `--watch`, until it exits), `import`, `use`, `init`, and `migrate` hold one project lock while they run. A second writer exits with the running command's name and process ID; retry after it finishes. `sync --check`, `sync --plan`, dry runs, `status`, `list`, and global commands take no lock. The lock works between processes on one machine, not across a network file system. The lock file, `.agnostic-ai/.command-lock`, stays on disk; leave it. The managed `.gitignore` block ignores it, and writing commands refresh those runtime entries even when generated outputs are committed. The operating system releases the lock when the process exits or is killed. ## Global flags | Flag | Description | |------|-------------| | `-h, --help` | Help for any command, same as `agnostic-ai help `. | | `--version` | Print the version and exit | | `-q, --quiet` | Errors only, plus the `~ kept` lines of `sync --keep-edits`, on stderr | | `-v, --verbose` | Increase output verbosity (repeatable). Mutually exclusive with `--quiet`. | | `--profile ` | Write a `runtime/pprof` CPU profile to `` (or set `AGNOSTIC_AI_PROFILE`). Read it with `go tool pprof `. | ## Exit codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | Any error (invalid file format, file read or write failure, missing config) | | verifier exit code | `verify` returns the external verifier's non-zero code unchanged. | ## Environment variables | Var | Default | Description | |-----|---------|-------------| | `AGNOSTIC_AI_HOME` | `~/.agnostic-ai` | Source folder for `sync --global`, `list --global`, `lint --global`, `validate --global`, and `migrate --global`, including their `local/` override layer. Project sync does not load it. See [global configuration](https://agnostic-ai.org/docs/configuration/#global-configuration). | ## Which config wins {#config-precedence} See [which value wins](https://agnostic-ai.org/docs/configuration/#precedence) in the configuration reference. # Start a project `use ` adds a tool and generates its files in one step; `init` and `import` let you review existing instructions first. `new` creates a spec, and `list` shows what loads. Run these from the project root, naming tools in agents and scripts to skip the interactive choice. ## use Add an AI tool using the project's existing specs: ```bash agnostic-ai use codex # switching to Codex this month agnostic-ai use claude codex cursor # a team on several tools ``` - With no `agnostic-ai.yaml`, it creates one enabling the detected tools plus the named ones, imports their instructions, skills, agents, hooks, and MCP servers into `.agnostic-ai/`, and asks about the `.gitignore` block, as `init` does. - In an existing project, it first imports each named tool's own config, such as a hand-written `AGENTS.md`, then adds the tool to `targets`. - If an import would replace a spec with content that tool never read, `use` writes no spec, lists each one, and names the `agnostic-ai import --overwrite` command. - If an import fails, `use` leaves that tool out of `targets` and names the retry command. It then syncs and shows what each added tool now reads: ``` ✓ codex now reads, from .agnostic-ai/: instructions AGENTS.md 1 skill .agents/skills/ review 1 MCP server .codex/config.toml github edit .agnostic-ai/ and run agnostic-ai sync to change what every tool reads ``` A tool already in `targets` changes nothing. A run that stopped partway finishes on the next try. A mistyped name suggests the closest one. To remove a tool, edit `targets` by hand. ### When use refuses or stops - A tool whose instructions file is in `sync.unmanaged` stops `use`, because importing it would copy that file to every tool. Import it by hand. - If `use` is interrupted before its imports finish, `sync` stops until `use` runs again. - It refuses to start a project inside an existing project's directory. - It refuses to add a tool when `agnostic-ai.local.yaml` sets `targets`, because that list wins. Add the tool there. ## init Create `agnostic-ai.yaml`, the managed `.gitignore` block, and `.agnostic-ai/AGNOSTIC_AI.md` with a placeholder note when it does not exist. Errors if `agnostic-ai.yaml` exists. It creates only the source folders `--demo` or `--preset` fill; `new` and `import` create the rest. ```bash echo "claude,codex" | agnostic-ai init agnostic-ai sync ``` | Flag | Description | |------|-------------| | `[dir]` | Base directory for the source folders (`.` for the old root layout). Writes matching `sources:` paths; the default `.agnostic-ai/` writes none. | | `--memory` | Enable shared memory. In Git with managed ignores, also write `memory.personal: repo` to a new `agnostic-ai.local.yaml`, never to an existing one. See [repo memory](https://agnostic-ai.org/docs/memory/#one-store-per-repository) for Kilo Code. | | `--demo` | Add example specs, one per source folder plus the `memory-curator` skill and two [spec guard hooks](https://agnostic-ai.org/docs/spec-format/hook-reference/#spec-guard). The [`no-force-push` hook](https://agnostic-ai.org/docs/spec-format/hook-recipes/#shared-hook-scripts) script goes to `.agnostic-ai/scripts/` whatever the base dir. Never overwrites files. | | `--preset ` | Add starter specs for a stack: `go`, `ts-react`, `python`. Combines with `--demo` and `--all`. Never overwrites files. | | `-a, --all` | Skip the target picker and enable every supported target. | | `--gitignore[=on\|off]` | On by default: generated outputs go into a managed `.gitignore` block; `off` commits them. `true` and `false` work too, here and on `sync`. Write the value after `=`: `--gitignore off` stops with a hint, since `off` would read as the folder. | | `--from ` | Then import existing config from this [import source](#import), or `all`. | | `--dry-run` | List the files it, and with `--from` the import, would write. | Without `--all`, `init` takes targets from a terminal prompt or a comma-separated list on stdin. Unknown names error and write nothing. The prompt ticks a default set (Enter accepts it), the first of these that has any tools: 1. Tools the project uses, by a marker such as `.claude/`, a root `CLAUDE.md`, `.codex/`, `.gemini/`, `.cursor/`, or `.github/copilot-instructions.md`. A root `AGENTS.md` is not a marker, since most tools read it. 2. Tools whose CLI is on `PATH`, as [`doctor install`](https://agnostic-ai.org/docs/cli-reference/check/#doctor) reports. 3. `claude` and `codex`. With no terminal and nothing piped (CI), or an empty piped line, `init` enables that default and names it. If a root `AGENTS.md` exists and `codex` is not in the set, it suggests enabling it. In a terminal, when the project has config an importer reads, or a root `AGENTS.md`, plain `init` offers to import it, as `--from all` does. Without a terminal, the summary lists the `agnostic-ai import ` commands instead. `--all` and `--dry-run` skip the question. The [first-sync picker](https://agnostic-ai.org/docs/cli-reference/sync/#first-sync-target-picker) ticks only the tools it detects. ## import Translate an existing AI CLI configuration into specs in the `sources:` directories, after `init`, writing nothing else. Preview diffs hide known [credential values](https://agnostic-ai.org/docs/cli-reference/sync/#credential-previews) on both sides. ```bash agnostic-ai import claude agnostic-ai import claude codex # in order; AGNOSTIC_AI.md comes from the last agnostic-ai import all agnostic-ai import claude codex --dry-run --diff # review content and conflicts ``` | Flag | Effect | |---|---| | `--dry-run` | List every file the import would write, without contents, from a temporary copy of the project. | | `--diff` | With `--dry-run`, show each destination as `create`, `change`, or `unchanged`, its sources, and a unified diff. Names each destination where sources disagree and the one a real import keeps (the last), still exiting 0. A protected spec still needs `--overwrite`. | | `--overwrite` | Replace existing specs with what the tool's files hold. Keys the tool never shows and comments on kept keys stay; `::target` blocks for other tools are lost. | ### Import behavior `import all` imports each tool it detects from config files and directories. A detected tool with no importer prints `skipping `. `all` cannot combine with other sources. - An entry file or skill folder that links outside the project is skipped with a `skipped ` note. Naming the tool (`import claude`) follows an entry file link. - Unedited sync output is skipped, so `import all` right after `sync` changes nothing. `import all` (and `init --from all`) reads a root `AGENTS.md` last and adds each `##` section that `.agnostic-ai/AGNOSTIC_AI.md` lacks, reporting `merged sections from AGENTS.md`. With no tool detected, a hand-written root `AGENTS.md` becomes `.agnostic-ai/AGNOSTIC_AI.md` (`seeded from AGENTS.md`); a generated one is left alone (`no importable AI CLI configs detected`). `import --global [targets]` reads your user config from the tools `sync --global` writes into `$AGNOSTIC_AI_HOME`: default model, effort, and [voice dictation](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) (`settings/imported.yaml`), Claude Code permission rules (`settings/permissions-claude.yaml`), MCP servers (`mcps/.yaml`), and supported [Kiro hook files](https://agnostic-ai.org/docs/targets/kiro/) (`hooks/.yaml`). - It leaves out anything a home spec provides, `local/` included. Existing specs stay, except for [voice additions](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation) and new rules appended to `settings/permissions-claude.yaml`. - Permission rules come only from `permissions.allow`, `ask`, and `deny` in `~/.claude/settings.json`, never managed or project settings. Rules sync recorded there are left out; rules with credentials or shell syntax it cannot inspect are left out with a warning. - It warns and skips a server another tool defined differently (the first tool wins), one it cannot convert, and one with a literal [credential](https://agnostic-ai.org/docs/spec-format/mcps/#what-import-writes), naming the server and field. A plain setting such as `NODE_ENV: production` still imports. - A full `sync --global` writes the imported MCP servers to every tool it manages. After a one-tool import, the printed `sync --global --target --check` passes while that tool's files match. When the home `targets` list leaves the tool out, import asks you to add it first. After an import from several tools, it points at `sync --global --dry-run`. ### Keep existing specs - Replacing a spec with content the tool never read stops the import with [AAI-203](https://agnostic-ai.org/docs/errors/#aai-203-import-would-replace-an-existing-spec), which lists the cases and fixes. A spec the tool already reads, unchanged since the last sync or that tool's import wrote it, may be replaced; that is how a native edit comes back. - `.agnostic-ai/AGNOSTIC_AI.md` never stops it. In `import claude codex` a later source still replaces an earlier one. - An identical spec keeps its content and modification time. After several sources import one file, only those whose content matches it may re-import without `--overwrite`. - A sync that keeps edited or unmanaged outputs keeps earlier records for unchanged specs, but does not make changed specs safe to replace. A disabled output, a skill asset left out, or settings with no translated fields do not count as read by the tool. ### Bring tool edits back - Re-importing tool files that still match the last sync keeps the spec exactly as written, including `::target` blocks, `workspaces`, comments, and overlays an older release wrote. - An edit in a tool's file goes back into the spec, keeping frontmatter keys the tool never shows (such as `workspaces` for Claude) and comments on kept keys, with `--overwrite` too. A spec with `::target` blocks stops with AAI-203 instead. - A skill or agent spec keeps keys the tool has no place for, such as Cursor's `argument-hint`. Deleting a key the tool does write reaches the spec: removing `model` from a Qoder agent removes it there. Rules are rebuilt from the native file. ### Files and source order - An absolute source path keeps its own destination. Links at the destination are kept, also in dry-run; a dangling link fails with its path. A conflict or interrupt restores writes made through symlinks and hard links. - The search for nested config skips git-ignored directories, directories with their own `.git`, and `node_modules/`. - Each source copies its top-level instructions file to `.agnostic-ai/AGNOSTIC_AI.md`, so the last argument wins. A fenced `AGNOSTIC_AI.md` stays when the entry point matches what sync would write from it; otherwise import overwrites it and warns. - If another entry point holds different hand-written content (an `AGENTS.md` next to `CLAUDE.md`), import warns that `sync` would overwrite it; merge it into `.agnostic-ai/AGNOSTIC_AI.md` first. `import all` merges a root `AGENTS.md` itself. - Valid sources: `claude`, `codex`, `cursor`, `aider`, `amp`, `warp`, `gemini`, `copilot`, `opencode`, `zed`, `antigravity`, `continue`, `cline`, `windsurf`, `junie`, `trae`, `kiro`, `crush`, `qoder`, `kilo`, `goose`, `factory`, `openhands`, `augment`, `ellipsis`, plus `all`. `jules` supports sync but not import. Each [target page](https://agnostic-ai.org/docs/targets/) lists what `import ` reads; see [Claude import](https://agnostic-ai.org/docs/targets/claude-reference/#import) for what `import claude` leaves in place. ### Filename prefix reclassification {#filename-prefix-reclassification} `import cline`, `import windsurf`, `import trae`, `import qoder`, and `import continue` reclassify each file in their rules directory by name: | Target | Rules directory | |--------|-----------------| | `cline` | `.clinerules/`, with `.cline/rules/` fallback | | `windsurf` | `.devin/rules/`, with `.windsurf/rules/` fallback | | `trae` | `.trae/rules/` | | `qoder` | `.qoder/rules/` | | `continue` | `.continue/rules/` | | Source filename | Becomes | |-----------------|---------| | `agent-.md` | `/.md` | | `skill-.md` | `/.md` | | `.md` | `/.md` | | `/.md` | nested under the same `` in the destination | Import strips the leading `# ` block the adapter adds and adds a `name:` field in frontmatter. ### import reconcile Plan how to bring later edits from a tool's skill folder into the source specs after an import: ```bash agnostic-ai import reconcile \ --base before-migration \ --migrated migration-commit \ --upstream other-branch \ --map .cursor/skills=.agnostic-ai/skills ``` | Flag | Commit or path | |------|----------------| | `--base` | Holds the original tool skill folder. An ancestor of `--upstream` and `HEAD`. | | `--migrated` | Holds the first complete source specs, so import's own changes are the baseline. An ancestor of `HEAD`, which supplies the current specs. | | `--upstream` | The branch with later edits to the tool skill folder. | | `--map =` | A native skill directory and `sources.skills`, separate and relative to the project directory, even when nested in a larger repository. Repeat per native directory. | | `--json` | Print the four resolved commit IDs and an `entries` array of `action`, `source`, and `destination`. | Each row reports `add`, `remove`, `update`, or `conflict`, then the tool and source paths, assets and executable modes included. Matching final files get no row. A conflict is a concurrent source edit, deletion, or addition at an occupied destination; a proposed file clashing with a surviving file above or below it (planned removals can allow a file-to-directory swap); or a change to a destination several mapped sources own. Conflicts do not change the exit status. It compares file contents stored in Git without merging text. Only direct skill folders with `SKILL.md` and regular files are supported; symlinks and special files stop planning, and every base file needs a source counterpart at `--migrated`. Commit changes in mapped trees first; other uncommitted changes are fine. It only reports a plan for you to apply: it copies no files, restores no skills, and leaves the index alone. ## list Print all loaded specs as `kindnamelayer`; built-ins add their name and the running version. With no specs, a hint goes to stderr and stdout stays empty. | Flag | Description | |------|-------------| | `--global` | List the specs in effect from `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`) and its `local/` overrides, including enabled built-ins. Works outside a project. | | `--json` | Print an `entries` array of `kind`, `name`, `path`, and `layer`. A built-in has an empty `path` and a `builtin` object with `name` and `version`. An empty scope prints an empty array. | ## new Create one spec in its configured `sources:` directory. Kinds: `agent`, `skill`, `rule`, `hook`, `mcp`, `command`, `settings`, `review`, `environment`, and `ignore`. Hooks, MCP, settings, and environments use YAML. Other kinds use Markdown. ```bash agnostic-ai new rule no-console-log # → /no-console-log.md agnostic-ai new hook fmt-on-save # → /fmt-on-save.yaml agnostic-ai new rule payments-context --scope services/payments ``` | Flag | Description | |------|-------------| | `--dry-run` | Preview the destination and exact content. | | `--scope ` | `new rule` only. Create a flat rule for a project-relative directory. See [scoped context](https://agnostic-ai.org/docs/scoped-context/). | It errors if the destination exists. Names are lowercase slugs (`[a-z0-9][a-z0-9-]*`). Replace the `TODO` description and body; `lint` reports the placeholder description as LINT031. Settings and environment templates hold only commented hints, and an ignore spec excludes no paths until you add patterns. # Generate output (sync) ## sync Generate files from your specs for each selected AI coding tool. A run that writes takes the [project lock](https://agnostic-ai.org/docs/cli-reference/#concurrent-commands); `--watch` holds it until it exits. ```bash agnostic-ai sync --only claude,cursor ``` | Flag | Description | |------|-------------| | `-t, --target ` | Comma-separated targets (default: all in config) | | `--only ` | Write only these targets. Errors on unknown names. | | `--except ` | Write all configured targets except these. Errors on unknown names. Not with `--only`. | | `--all` | Write every configured target without the [first-sync picker](#first-sync-target-picker). | | `--dry-run` | Print to stdout instead of writing. Does not preview leftover deletions. | | `--plan` | Print added and changed counts per target without writing. Exits 0, or 1 on drift with `--check`, so CI can gate on the short report. Ignores `--diff` and `--format`. | | `--check` | Exit non-zero if files on disk differ from what sync would write. Writes nothing. | | `--against ` | With `--check`, compare the staged (`index`) or last committed (`HEAD`) specs with the outputs Git holds there, not the working tree. See [`--against` details](#against-details) and [git hooks](https://agnostic-ai.org/docs/git-hooks/#check-staged-files). Not with `--plan`, `--watch`, or `--global`. | | `--diff` | With `--check`, print a diff per changed file, on disk vs what sync would write. Fails without `--check` or `--dry-run`. | | `--format ` | With `--check`: `human` (default) table or `github` Actions annotations. `--json` wins. | | `--backup` | Copy each existing output file to `.bak` before overwriting, with the file's own permissions. When the managed `.gitignore` block is on, it lists the `.bak` of each output it ignores, and `.git/info/exclude` lists any other backup sync made, such as one whose output is no longer generated. Pair with `revert`. | | `--keep-edits` | Write every output except those edited since the last sync (with no record, tracked outputs that differ from `HEAD`), and print `~ kept ` for each (stderr under `--quiet`). Exits 0. For [git hooks](https://agnostic-ai.org/docs/git-hooks/#regenerate-on-checkout). Not with `--check`, `--plan`, `--watch`, or `--global`. | | `--untrack` | Run `git rm --cached` on generated paths that git tracks and ignores, keeping the working copy. Not with `--check`, `--plan`, `--dry-run`, `--watch`, or `--global`. | | `--gitignore ` | Override `gitignore.enabled` for this run. | | `--watch` | Stay running and [re-write on changes](#watch-mode). Not with `--check`. | | `--watch-poll` | With `--watch`, poll every 200 ms, for network mounts or container volumes. | | `--jobs ` | Targets written in [parallel](#parallel-emission): `0` (default) is one per CPU, `1` is serial. | | `--json` | Print [JSON](#json-output). Not with `--watch` or `--diff`. | | `--global` | Install user-level specs from `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai/`) into each tool's user config. See [global configuration](https://agnostic-ai.org/docs/global-configuration/). | ### What --against compares - Only outputs Git tracks are compared. An untracked `agnostic-ai.local.yaml` or `.agnostic-ai/local/` is not read. - Files are read with the line endings Git stores, so `core.autocrlf` and `core.eol` cause no drift. An `eol` rule in `.gitattributes` still applies, as it does in every clone. - Shared skill links that Git checks out as plain files fail with the affected link and repair choices. See [shared skill checkout policy](https://agnostic-ai.org/docs/configuration/#syncshared-skills). Any other link checked out as a plain file fails with a message that names `core.symlinks`. - A tracked output that no spec produces fails as a leftover to delete: a file with the generated header, or one that still holds what the previous state wrote (`HEAD` for `index`, the first parent for `HEAD`). A file edited since is left alone. A run narrowed with `-t`, `--only`, or `--except` skips this comparison. - A hand-written skill, agent, rule, or command that Git tracks inside a folder the managed block ignores also fails, because only the tool that reads that folder sees it. The failure names the `import` that adopts it. For example, a branch from before the move to `.agnostic-ai/` adds `.cursor/skills//SKILL.md`: run `agnostic-ai import cursor`, then `git rm --cached` the file, or list it under `sync.unmanaged` to keep it. `sync --global` works outside a project and never loads project config or packs. It loads overrides from `local/` in the source root and reads `targets` and `on-unsupported` from an optional `agnostic-ai.yaml` there. - Accepts `--target`, `--only`, `--except`, `--dry-run`, `--check`, `--check --diff`, `--backup`, `--plan`, and `--json`. Rejects `--watch`, `--gitignore`, `--jobs`, and `--plan` with `--check` or `--dry-run`. - `--plan` lists each file a sync would create, update, or delete, with key-level settings and MCP edits. `--json` adds a `keys` list per settings or MCP file. - Files per tool: [global output](https://agnostic-ai.org/docs/global-configuration/#global-output), [MCP servers](https://agnostic-ai.org/docs/global-configuration/#global-mcp-servers), and [model, effort, and permissions](https://agnostic-ai.org/docs/global-configuration/#global-default-model-and-effort). **Summary.** With nothing to do, `sync` prints `✓ 26 targets up to date · 49ms`. After an edit it names the changed specs, then the files created (`+`), updated (`~`), and removed (`-`): ``` ~ rule testing → 9 files in 9 targets + .cursor/rules/testing.mdc .kiro/steering/testing.md (+1 more) ~ AGENTS.md CLAUDE.md GEMINI.md (+2 more) ! 1 file to commit: .openhands/setup.sh ✓ synced 26 targets · 3 created · 6 updated · 49ms ``` Each list shows three paths (`-vv` lists all). A `config` spec means `agnostic-ai.yaml` changed. The `!` line, absent outside a git repository, lists changed files git tracks or does not ignore. **Tracked despite ignored.** For a generated path git both tracks and ignores (usually committed before the managed `.gitignore` block), `sync` prints `! 1 file tracked despite being ignored: git rm --cached .claude/rules/tone.md`. `sync --untrack` runs that command for every such path and reports `~ untracked `. `doctor` reports it but never fails on it alone. **Hand edits.** `sync` saves an output edited by hand since the last sync as `.bak` before overwriting or removing it. For an overwrite it prints `! overwrote a hand edit to (saved as .bak)` (on stderr under `--quiet`). `revert` puts the edit back; move it into `.agnostic-ai/` to keep it. With the [`memory` built-in](https://agnostic-ai.org/docs/memory/), the line for an instructions file such as `GEMINI.md` also suggests saving a fact in `.agnostic-ai/memory/`, since a tool's own memory may have written the edit. `--keep-edits` leaves the edit in place instead. ### Hand edits: existing backups and merged settings - When `.bak` already exists, sync leaves the new edit in place and says so. An earlier backup is never replaced. - A key you add to a merged settings file, such as `.claude/settings.json`, is kept in place, not backed up. **Typos.** `sync` and `sync --check` stop on a hook event that looks like a typo, such as `PreToolUze`, and name the closest; another tool's event passes in any case or snake_case. An agent `skills:` name close to a project skill, such as `pr-swep`, only warns, since it may be a user or plugin skill. Pack specs are left to `validate`. **Hand-written instructions.** `sync`, `--check`, `--plan`, and `--json --dry-run` stop when an instructions file such as `CLAUDE.md` or `AGENTS.md` holds text agnostic-ai did not write and neither `.agnostic-ai/AGNOSTIC_AI.md` nor `.agnostic-ai/local/` has, and name the `import` that keeps it. `sync --backup` replaces the file and keeps the old one as `.bak`, unless that `.bak` exists. A file an earlier sync wrote, or with the generated header, never stops it, nor does a tracked file under `--keep-edits`. **Remove leftover files.** `sync` records every file it writes in `.agnostic-ai/.sync-state`. A full run deletes files, and then empty directories, it no longer writes, if it can prove it wrote them (generated-file header or recorded hash). A generated file edited since sync is saved as `.bak` before removal. A headerless file edited since sync, or written by a sync that recorded no checksum, is kept as `~ kept orphan ` with the reason. Kept orphans stay in the managed `.gitignore` block and count as drift until removed or listed under `sync.unmanaged`. In a terminal, `doctor --fix` offers to remove them, defaulting to no. When a configured target's external adapter is not on PATH, sync keeps that target's files and shared skill links and prints `~ skipped removing stale files: did not load; a sync that loads every target removes them`. Changing only a rule, skill, or agent name's capitalization keeps the newly written outputs on case-insensitive disks. Hand-edit backups and `--keep-edits` still apply. On case-sensitive disks, sync removes the old spelling's files. ### Find leftover files without .sync-state (fresh checkout) With no `.sync-state`, as in a fresh checkout of a repo that commits generated files, `sync --check` and `doctor` scan Git-tracked files. A tracked file is a leftover when: - It sits where a configured target writes, and its first line carries the generated-file header. - It still holds exactly what the last commit's specs would write, such as a headerless `.claude/launch.json` whose spec you deleted but have not committed. - After that deletion is committed, a headerless JSON file still holds exactly what the commit that last changed it would write. Up to eight past commits are checked per run, and you delete these by hand. A plain full `sync` removes none of these. It records them under target `unledgered` and lists each as `~ kept leftover ` (`--only` and `--except` record them without naming them). An empty `.sync-state` still counts as a record and turns this scan off. `doctor --fix` removes a leftover in a tool directory or root dotfile; delete a scope document such as `services/api/AGENTS.md` by hand, or add it to `sync.unmanaged`. ### Warning guidance {#warning-guidance} Unsupported global agents get one `fix:` per run: set `targets:` in `$AGNOSTIC_AI_HOME/agnostic-ai.yaml` (or `local/agnostic-ai.yaml`), or `targets-exclude:` on each agent. These field notes add a next step; guidance never edits specs, and repeated project notes stay hidden until they change or you pass `-vv`. | Field | Next step | |-------|-----------| | Skill `argument-hint` | Move it under each keeping tool's override block (today only `x-claude`). Put guidance other tools need in the description or body. | | Skill `metadata` | Move it under each keeping tool's override block (today `x-claude`, `x-cursor`, `x-opencode`). Shared global skill files omit overrides, so use the description or body. | | Skill `disable-model-invocation` | Use a native invocation policy, or exclude tools that cannot enforce manual invocation. Excluding a tool does not hide a copy another tool installs in a shared global skills directory it reads. | | Agent `readonly` | Configure and verify native permissions or session settings, or exclude tools that cannot enforce read-only access. | | Codex agent `sandbox_mode` | Enforce it in the parent session's `config.toml` (for read-only, `sandbox_mode = "read-only"`), then set `x-codex: {sandbox_mode: null}` to omit the inactive key. Exclude the agent from Codex if the session cannot enforce it. | ### First-sync target picker On the first `sync` (no `.agnostic-ai/.sync-state` yet), if the config still lists every supported target, `sync` asks which to keep and saves the choice to `agnostic-ai.yaml`. `echo "claude,codex" | agnostic-ai sync` keeps two. | Context | Behavior | |---------|----------| | Interactive terminal (TTY) | Multi-select prompt, same as `init`. | | Piped stdin | Selects and saves without a prompt. | | No interactive terminal, nothing piped (CI) | Writes every configured target. | | `--all`, `-t`, `--only`, or `--except` | Skips the picker for this run. | ### Reading a failing `--check` {#reading-a-failing---check} A failing `--check` exits non-zero in every format. Stderr names the fix, `agnostic-ai sync`, and points at `agnostic-ai doctor`. Each changed file gets a label: | Label | Meaning | |-------|---------| | Out of date | The file still holds what the last sync wrote, so the specs changed. Without a record of the last sync, as in fresh CI, every changed file reads this way. | | Edited locally | The file changed since the last sync. The next sync saves it as `.bak`, then writes over it. | | Line endings only | Git checked the file out with CRLF line endings. Run `git config core.autocrlf false`, or add a `.gitattributes` rule such as `* text eol=lf`, then run `agnostic-ai sync`. With `--against`, Git stores the file with CRLF or an `eol` rule converts it: drop the rule, then stage or commit the regenerated file. | A spec or output that Git tracks as a link and checked out as a plain file, as it does with `core.symlinks=false`, gets a message naming that setting. A file several targets read, such as `.agents/skills//SKILL.md`, is listed once, under the first target with `(shared with )`, and counted once in `--diff`, `--format=github`, and `status`. `--json` keeps one record per target. `--diff` prints changed lines, a one-line summary for a missing file, and truncates a large diff with a count. `--format=github` prints `::error file=...,line=...::` annotations on the pull request. Neither has a config-file key. The once-per-run [LINT038 permission note](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules) goes to stderr with `--json`. ### Credential previews Dry-run, render, import, and migration previews hide literal values in known credential fields, environment maps, headers, and credential flags, assignments, and URLs in recognized commands. Plain environment references stay visible; ones with defaults or mixed literal text are hidden. A hidden-value change still reports drift. - JSON, JSON with comments, YAML, and TOML previews keep valid syntax; a recognized document or sensitive command that cannot be shown safely is withheld. - API-key helper paths stay visible, but helper commands with literal arguments or shell operations are hidden, since they can contain the key. - Written files, captured content, and tracking checksums keep the real values. Secrets in freeform prompts, prose, unknown scripts, or binary assets are not detected. ### Watch mode `sync --watch` watches `agnostic-ai.yaml`, `agnostic-ai.local.yaml`, `.agnostic-ai/AGNOSTIC_AI.md`, `.agnostic-ai/local/`, `.agnostic-ai/overlays/`, and every `sources.*` directory (absolute paths outside the project and linked roots included), even ones that appear later. It groups changes within 50 ms and exits on Ctrl+C. A spec change re-syncs only the targets that write that kind; config and overlay edits, deletes, and renames re-sync everything and update the watched paths. It polls every 200 ms where notifications fail or a missing external source has no safe parent to watch (never a parent holding the project), catching edits made during a re-sync on the next tick. ### Parallel emission {#parallel-emission} Each target gets its own worker; `--jobs ` bounds how many run at once (no config-file key). The output never depends on it. Use `--jobs 1` only to debug or pin ordering. ### Profiling a slow sync `sync -v` shows counts and elapsed time for targets that wrote files, and summarizes unchanged targets. `sync -vv` shows every target, path, and repeated coverage note, including accepted notes; it runs sync again. `--profile ` or `AGNOSTIC_AI_PROFILE=` writes a CPU profile. Under `--jobs > 1` target times overlap. ### JSON output `sync --json`, also with `--plan`, `--dry-run`, or `--check`, writes one object to stdout, without summaries or verbose diagnostics. The first-sync picker still saves your choice, prompting on stderr. | Field | Description | |-------|-------------| | `version` | JSON format version, currently `"1"`. A breaking change bumps it. | | `command` | `"sync"`, `"sync --plan"`, `"sync --dry-run"`, or `"sync --check"`. | | `writes` | Files written (`"create"`, `"update"`), orphans removed (`"delete"`), shared skill folders linked or retargeted (`"link"`), or, for `--check`, files needing attention (`"missing"`, `"stale"`: the specs changed, `"edited"`: changed since the last sync, `"orphan"`, or `"leftover"`: no longer generated, removed by the next full sync, target `ledger`, or `unledgered` when no record proves sync wrote it). With `--untrack`, a path removed from the index is `"untracked"`, target `agnostic-ai`. A write over or removal of a hand edit names its copy in `"backup"`. | | `skipped` | Files already matching or links unchanged (`"skip"`), user-owned (`"unmanaged"`), orphans sync cannot prove it wrote (`"orphan"`), unledgered leftovers kept (`"leftover"` for `doctor --fix` to remove, `"orphan"` for a scope document), or, with `--keep-edits`, hand edits left in place (`"edited"`). A path git tracks and ignores, without `--untrack`, is `"tracked"`, target `agnostic-ai`. Empty for `--check`. | | `warnings`, `notes` | The capability warnings and coverage notes a plain `sync` prints, one entry per target, complete on every run (also notes a plain `sync` hides as unchanged), minus [accepted notes](https://agnostic-ai.org/docs/configuration/#coverageaccept). Each has `target`, `kind`, `count`, `message`, and an optional `suggestion` (the plain `fix:`). A project-wide note has target `agnostic-ai`, empty `kind`, and `count` 0. | | `errors` | Per-target errors with `target` and `message`. `sync --json` goes on with the other targets. A failed target keeps what it wrote before the error, listed in `writes` and `.sync-state`. | `writes` and `skipped` entries have `target`, `path`, `action`, and `bytes`, for example `{"target": "claude", "path": "CLAUDE.md", "action": "create", "bytes": 1284}`. `--plan --json` and `--dry-run --json` write nothing and exit 0. A leftover they would remove is `"delete"`; kept files are `"orphan"` or `"leftover"`. `--dry-run --json` also lists unchanged outputs as `"skip"`. Count `writes` by `target` for the `--plan` numbers. `sync --global --json` and its previews carry `warnings` and `notes` too, or send them to stderr when a run fails before printing JSON. # Check specs and output Start troubleshooting with [`doctor`](#doctor): it combines project and local setup checks and ends with a next step. It is read-only unless you pass `--fix`. Use [`validate`](#validate) for invalid spec formats, [`lint`](#lint) for spec quality, and [`sync --check`](https://agnostic-ai.org/docs/cli-reference/sync/#sync) for generated files that differ from the sources. [`status`](#status) summarizes the project. [`verify`](#verify) runs your own tasks to check each AI tool's behavior. `validate` and `lint` skip built-in hooks whose target selectors match no selected tool. User-authored hooks get the usual checks. ## validate Load all specs and report parse errors. On success it prints `loaded 12 entries. ok.`. With no specs it prints `loaded 0 entries. ok.` and stderr suggests `init` or `import`. | Check | Reports | |-------|------------| | Hook events | A hook with neither `on:` nor `event:`, or an event no configured target supports. The report lists the supported events. | | Unused spec kinds | Hook or MCP specs that no enabled target reads. The line names the targets that would. | | Declared sources | A `sources.` path in `agnostic-ai.yaml` with no directory. It prints a `note:` on stderr, loads the kind empty, and passes. | | Tool-specific instruction blocks | A `::target` or `::targets` name in `.agnostic-ai/AGNOSTIC_AI.md` that is neither a built-in target nor an external adapter listed in `targets`. Also a tool that reads no entry-point file (`cursor`, or any target with `outputs..rules-file`). | | Global rules | With `--global`: a rule with scope, path, glob, or target conditions, which `sync --global` rejects. | | Global settings | With `--global`: a settings `effort` that a tool cannot take, such as `max` for Claude. | | Flag | Description | |------|-------------| | `--fix` | Rewrite source spec files to repair issues it can fix. | | `--global` | Validate the specs in `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`) and its `local/` overrides. Hook events are checked against the tools `sync --global` writes hooks for, narrowed by a `targets` list in the home config. Works outside a project. | For event names, see [portable hook events](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) and each [target page](https://agnostic-ai.org/docs/targets/). Use `on:` to share an event across tools. ## lint Check spec quality after parsing. Exits 1 on errors. Each code, and the context budgets, are on [lint codes](https://agnostic-ai.org/docs/cli-reference/lint-codes/); `agnostic-ai explain LINT011` prints one code's cause, fix, and config key. ```bash agnostic-ai lint --strict ``` | Flag | Description | |------|-------------| | `--strict` | Exit 1 on warnings too, for CI. | | `--suggest-capabilities` | Suggest neutral names for tool aliases (LINT037). Off by default. | | `--global` | Lint the specs in `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`) and its `local/` overrides. Also reports LINT010 and LINT014. Budgets come from the `lint` key in the home config. Works outside a project. | | `--json` | Print `{version, command, findings}` on stdout. Each finding has `code`, `severity` (`error` or `warn`), `path`, and `message`, like the `lint` list in `doctor --json`. Works with `--global`. | | `--files ...` | Report only findings on these files, or on specs below a directory. `-` reads paths from stdin, one per line. A missing path, or no path, fails. Not with `--global`. The [spec guard hook](https://agnostic-ai.org/docs/spec-format/hook-reference/#spec-guard) uses it after each edit. | Fix the named source, rerun lint, then sync if generated output changes. ## verify Run your own checks against each selected AI tool. Omit `--target` to check every configured target. It first runs `sync --check`; a missing or stale generated file stops it before any verifier runs or any result is written. ```bash agnostic-ai verify --target codex ``` The command in [`verify.command`](https://agnostic-ai.org/docs/configuration/#verify) runs once per target, without a shell, and gets this JSON on stdin. Stdout and stderr pass through. A non-zero exit stops the run and becomes the `agnostic-ai` exit code. | Field | Meaning | |------|---------| | `version` | Input format version, currently `1`. | | `target` | Tool being verified, such as `"codex"`. | | `configured_model` | Optional. Model in the generated config, not the one used at runtime. | | `cli` | Optional. Detected CLI command, resolved path, and `--version` output. Omitted when the binary is missing. | | `harness_fingerprint` | SHA-256 checksum of the tool's specs and generated files. It identifies the setup. | ### Record and compare runs Recording is optional. Pick a private destination and an evaluation identifier that names the same task suite, dataset revision, and evaluation rules for both runs: ```bash agnostic-ai verify --target codex --evaluation smoke-v1 --record before.json # Change an instruction, then regenerate the tool configuration. agnostic-ai sync agnostic-ai verify --target codex --evaluation smoke-v1 --record after.json agnostic-ai verify compare before.json after.json [--json] ``` | Flag | Effect | |------|--------| | `--record ` | Save one run at this path. Parent directories must exist. | | `--evaluation ` | Name the task suite and dataset revision. Required with `--record`. | | `--overwrite` | Allow replacing an existing result file. Requires `--record`. | Each target in a comparison gets one result: | Result | When | |--------|------| | `regression` | The first run passed and the second failed. | | `recovery` | The first run failed and the second passed. | | `unchanged` | Both runs have the same outcome. | | `missing_target` | The target is in only one file. | | `non_comparable` | The verifier identity is unknown or changed, the evaluation identifiers differ, or the target was skipped. | | `execution_error` | The verifier failed to launch. | A changed harness fingerprint names the configuration change, not a failure. The elapsed difference is the second duration minus the first (nanoseconds in JSON); one comparison does not show a statistical improvement. `verify compare` exits 0 when it can read both files, regressions included, and 1 on an invalid or unsupported file. The files use schema `version: 1` with `started_at` (UTC RFC 3339), `evaluation`, `verifier_identity`, and `targets`. Each target holds the input fields above, `elapsed_ns`, `outcome`, and an optional `exit_code`. The outcome is `passed` (exit 0), `failed` (non-zero exit), `execution_error` (launch, cancellation, or context preparation failed), or `skipped` (not attempted after an earlier failure; no context or exit code, duration `0`). The CLI and model fields do not establish the model used at runtime. `verifier_identity` hashes the resolved executable and the configured arguments. For a known interpreter, a direct script argument adds its code; options before the script, module loading, and inline code leave the identity unknown. Other file arguments do not change it. Change the evaluation identifier when tasks, datasets, remote dependencies, or rules change without changing the executable, arguments, or script. Raw arguments, verifier output, and environment values are not saved; credentials in CLI and model metadata are redacted, and an evaluation identifier holding a detected credential is rejected. Files are written in one step with mode `0600`, which needs a filesystem with hard links for a new file. If saving fails, a failed evaluation keeps its exit code and a passing one exits 1; the save error is reported separately. Pass rates, token use, and cost are not inferred. ## doctor ```bash agnostic-ai doctor ``` Read the findings and the final **Next step** before a repair or a focused check. The [troubleshooting guide](https://agnostic-ai.org/docs/troubleshooting/) covers symptoms. Doctor reports four kinds of generated file: - **missing**: never synced - **stale**: out of date with the specs - **edited**: changed since the last sync - **orphaned**: no longer generated, kept because sync cannot prove it wrote the file It exits non-zero on any drift, [lint](#lint) error, or untrusted, modified, or unreadable Codex hook. Hooks you disabled on purpose do not fail. It also names any [spec migration](https://agnostic-ai.org/docs/cli-reference/upgrades/#migrate) that applies. | Flag | Description | |------|-------------| | `-t, --target ` | Comma-separated targets (default: all in config). An unknown name fails with AAI-301, as in `sync`. | | `--scope ` | `all` (default) includes local Codex hook trust and your own Claude settings files. `project` skips them and keeps all project checks. | | `--fix` | Write missing files and overwrite stale or edited generated files. Remove each nested `CLAUDE.md` whose text is already in a scoped rule. In a terminal, ask before removing each kept orphan and each group of [Claude skill overrides](#claude-skill-overrides), with No as the default. Without a terminal, keep them and explain why. Exit non-zero while any orphan remains. | | `--backup` | With `--fix`, copy each file to `.bak` before overwriting it, removing a confirmed orphan, or removing confirmed skill overrides. | | `--check-globs` | Flag rules whose `globs:` match no files. Off by default. | | `--check-references` | Flag relative Markdown links in generated skills whose file is missing. Off by default. | | `--json` | Drift report as JSON, same schema as `sync --check --json`, plus the lists below. It fails on the same findings as text mode. It cannot be combined with `--fix`. | On a fresh CI runner, run `agnostic-ai doctor --scope project --check-references`. Codex hook trust then shows `SKIPPED` in text, and JSON adds `hook_trust_check: {"status": "skipped", "reason": "project scope skips local runtime trust"}` with an empty `hook_trust`. `--scope project --fix` repairs drift without reading or approving trust. `--json` stays read-only. `--check-references` reads each Markdown file a selected tool writes for its skills. A link is valid when it resolves from the document's directory or from the project root. It skips code, URLs, absolute paths, and `#fragment`-only links, and checks only the file in a `file#fragment` link. [`doctor.check-references.ignore`](https://agnostic-ai.org/docs/configuration/#doctorcheck-referencesignore) exempts destinations that can never resolve. Any broken link fails doctor. Findings group by source spec and link: ``` Skill references: ✗ .agnostic-ai/skills/deploy/SKILL.md:8 links to missing references/setup.md targets: claude, codex ``` | `--json` list | Fields | |------|--------| | `references` | With `--check-references`: `target`, `source` (omitted when unknown), `path`, `line`, `destination`. | | `globs` | With `--check-globs`: one item per rule that never loads: `rule`, `globs`, and `invalid` (the pattern doctor cannot read) when that is the reason. | | `nested_copies` | `path` and `rule` for each nested `CLAUDE.md` a rule already holds. | | `hook_script_drift` | `script` (the basename) and `tools` for each hook script whose body differs across tools. | | `lint` | `code`, `severity` (`error` or `warn`), `path`, `message`. | | `hook_trust` | `path`, `event`, `group`, `handler`, `hook`, `status` (`untrusted`, `modified`, `disabled`, or `unknown`). An `unknown` entry adds `problem` with the failed check. | | `packaging_ignore` | `path`, and either `uncovered` (generated paths) or `problem` (why coverage could not be checked). | | `skill_overrides` | With `claude` selected, one item per `skillOverrides` entry, highest file first: `path` (absolute outside the project), `scope` (`local`, `shared`, or `user`), `skill` (the key as written), `value` (the string, or the JSON text of another value), `synced` (false for a skill sync does not write), and `effective` (false when a higher file sets the skill). An unreadable file has one item with `path`, `scope`, and `problem`. | After the file report, doctor prints: | Block | What it shows | Counts as drift | |-------|---------------|-----------| | **Spec health** | The findings `agnostic-ai lint` reports. An error fails doctor; a warning does not. | No | | **Coverage notes** | With [`coverage.accept`](https://agnostic-ai.org/docs/configuration/#coverageaccept) set, how many notes it accepts (`coverage_accepted` in `--json`). `sync -vv` lists them. | No | | **Packaging ignores** | Generated paths that an existing root `.npmignore`, `.vscodeignore`, or `.dockerignore` does not cover, with the ignore file. | No, reported only | | **Tracked despite ignored** | A generated path that git tracks and ignores, with the `git rm --cached` fix. | No | | **Codex hook trust** | With Codex selected: inactive handlers in the project hooks file and user `hooks.json`, with `/hooks` as the next step. Reads trust from `CODEX_HOME/config.toml` (default `~/.codex/config.toml`). Untrusted, modified, or unreadable status fails; disabled does not. | Never grants trust | | **MCP** | Whether each stdio `command:` is on PATH, with install hints. Then each `${NAME}` a server reads in `env`, `headers`, `url`, or `args` that is unset in this shell, by name only. A reference with a default is skipped. | No | | **Claude hook specs** | With `claude` enabled: a hook spec with several commands that `.claude/settings.json` already runs in one group with different settings, as an older `import claude` wrote it, so each command runs twice. Delete the spec, run `sync`, then run `import claude`. | No | | **Nested CLAUDE.md** | With `claude` enabled: a hand-written `/CLAUDE.md` whose text equals the body of a rule scoped to ``, as `import claude` leaves it. Claude Code would load it beside the synced rule. | Yes, `--fix` removes it | | **Script divergence** | Scripts under `.agnostic-ai/scripts//` with the same name but different bodies across tools, with the suggested path `.agnostic-ai/scripts/`. | Yes, no automatic fix | | **Unmanaged config** | Markdown and TOML config files with no generated-file marker, grouped by the `import` source that adopts each. | No | | **User-owned** | [`sync.unmanaged`](https://agnostic-ai.org/docs/configuration/#syncunmanaged) entries, left out of Unmanaged config. | Never | | **Global names** | A project skill or agent that shares its name with one in `~/.agnostic-ai/`, and which wins per tool. A name in [`sync.allow-global-names`](https://agnostic-ai.org/docs/configuration/#syncallow-global-names) shows as allowed. See [shared names](https://agnostic-ai.org/docs/global-configuration/#global-shared-names). | Never | | **Instructions** | A hint when `AGNOSTIC_AI.md` still holds the long default text. | Never | | **Claude skill overrides** | With `claude` selected: synced skills that `skillOverrides` in a Claude settings file turns off, hides from Claude, or lists without a description. See [below](#claude-skill-overrides). | Never | ### What the packaging check covers The check handles literal paths, `*`, `?`, character classes, and `**` segments. Braces, extended globs, escaped patterns, and unsupported classes give a coverage-check warning, not an uncovered claim. It reads only the root ignore file. It ignores `package.json` allowlists, built-in package exclusions, nested ignore files, and Dockerfile-specific ignore files. Verify with `npm pack --dry-run`, `vsce ls`, or your Docker build context. ### Claude skill overrides Claude Code's `/skills` menu saves each skill you switch off in your git-ignored `.claude/settings.local.json`. The skill still syncs, but Claude Code refuses to run it. Claude Code also reads `skillOverrides` from the shared `.claude/settings.json` and your user `~/.claude/settings.json` (under `CLAUDE_CONFIG_DIR` when set). Doctor lists the synced skills each file turns off, hides, or lists without a description, highest file first: local, shared, user. ```text Claude skill overrides: ! .claude/settings.local.json turns off 1 of 3 synced skills (off), so Claude Code refuses to run them: deploy → review them in Claude Code with /skills, run `agnostic-ai doctor --fix` in a terminal, or delete the entries from .claude/settings.local.json ! .claude/settings.json turns off 1 of 3 synced skills (off), so Claude Code refuses to run them: lint → team policy: set skillOverrides in .agnostic-ai/overlays/claude.settings.json and run `agnostic-ai sync` ! ~/.claude/settings.json hides 1 of 3 synced skills from Claude (user-invocable-only): deploy → applies to every project on this machine; delete the entries from ~/.claude/settings.json ~ ~/.claude/settings.json also sets 4 entries for skills sync does not write ``` An entry is reported only when no higher file sets the same skill: a user file that turns `deploy` off is not reported when the local file turns it back `on`. Entries for skills sync does not write, such as your personal ones, are only counted. Skills whose generated files are under `sync.unmanaged` do not count as synced. `--json` lists every entry, with `effective` and `synced` as above. The shared file is team policy, so its line points to the [overlay](https://agnostic-ai.org/docs/spec-format/overlays/) sync writes it from, or is a `~` note when the overlay sets the same value. Below the git root, Claude Code also reads the local file at the root (or the main checkout in a worktree), and its value wins for a key both set. Doctor lists it first, labelled `(git root)` or `(main checkout)`. Claude Code and doctor skip the root file on Windows and when the root is your home folder. Claude Code also skips it when you do not own the root or its `.git` or `.claude` entry; doctor does not check ownership. `--scope project` reads only the shared file. An empty settings file counts as no settings. Managed (enterprise) settings and `--settings` are not read. Claude Code reads settings as strict JSON, and so does doctor: | Case | Doctor reports | `--json` | |------|----------------|----------| | The file is not strict JSON (a comment or trailing comma), so Claude Code skips it | The file and the parse error | One item with `problem` | | A value other than `on`, `off`, `user-invocable-only`, or `name-only`, which Claude Code rejects and may skip the whole file for | The value's JSON text. The file's other entries get a caution line and overrule nothing in a lower file. | `rejected: true`, and `uncertain: true` on the other entries | | Two keys name one skill (`Deploy` and `deploy`) with different values, and Claude Code does not say which applies | Both keys. The file still overrules lower files. | `conflict: true` on both | | Two keys name one skill with the same value | One key | The other has `effective: false` | `doctor --fix` in a terminal asks once per state whether to re-enable those skills. A yes removes only their entries (both keys of a duplicate) from the project's `.claude/settings.local.json`, the only file it edits. When a lower file sets the same skill, the question names the value that then applies (or may apply, if that value is rejected). It skips a linked, hard-linked, read-only, or other user's file, one that sets `skillOverrides` twice, and one under [`sync.unmanaged`](https://agnostic-ai.org/docs/configuration/#syncunmanaged). The file is replaced in one step, so a failed write leaves it as it was, and a file that changed while doctor ran is left alone. Subcommands run one check: | Subcommand | Check | |------------|-------| | `doctor config` | Validate `agnostic-ai.yaml` with the checks every command runs when it loads the project. | | `doctor install` | Which AI CLIs are on PATH. | | `doctor mcp` | Find each MCP server's command and list its unset `${NAME}` references. Fails when the config does not load. | | `doctor rtk --command ''` | Preview an RTK rewrite and compare declared Claude Code approval rules. Add `--json` for JSON. | ### RTK approval diagnostics `doctor rtk --command 'git status'` calls the installed `rtk --version` and `rtk rewrite`, following [RTK's rewrite protocol](https://github.com/rtk-ai/rtk/blob/v0.51.0/src/hooks/rewrite_cmd.rs). It never runs the command or changes permissions. Missing RTK reports `missing`; an unsupported or already-prefixed command reports `unchanged`. RTK's exit 3 with a replacement (it requests approval) and exit 2 without one (it found a deny rule) are reported as results, with `processor_exit` in JSON. Other RTK failures and unreadable settings fail the command. The report compares simple Bash rules declared in planned project settings (or native project settings when none is planned), project-local settings, and user settings (`CLAUDE_CONFIG_DIR` or `~/.claude`), using [Claude Code's precedence](https://code.claude.com/docs/en/permissions): deny over ask over allow. Exact rules, bare `Bash`, and simple trailing command prefixes are supported; shell wrappers, parameter rules, other shell syntax, and ambiguous patterns report `unknown`. A rewrite from a known ask to a known allow warns. It lists known RTK `PreToolUse` handlers by settings path and warns when there is more than one. It recognizes direct Bash handlers that use `rtk rewrite` or RTK's Claude processor; plugins, indirect scripts, and other matchers can go unseen. Runtime approval always reports `unknown`: administrator settings, session options, project trust, enabled hooks, and other handlers can change the outcome on a live host. Warnings do not grant approval or fail the command. ## status Show project configuration and sync state. It exits 0 even on drift; in CI, use `sync --check` or `doctor`. ```bash agnostic-ai status [--json] ``` ``` Project: my-project Layers: project (.agnostic-ai/) Specs: 3 rules, 1 agent, 2 skills Targets: claude, cursor, copilot Last sync: 2026-05-07 14:22 (5 files changed) Drift: in sync ``` | JSON key | Type | Description | |-----|------|-------------| | `project` | string | Project directory base name. | | `layers` | array | Active spec layers, each with `name` and `path`. | | `specs` | object | Counts: `agents`, `skills`, `rules`, `hooks`, `mcps`. | | `targets` | array | Targets in `agnostic-ai.yaml`. | | `last_sync` | string or null | RFC 3339 time of the last successful `sync`. Falls back to the newest generated file's modified time. `null` (`unknown` in text) with no files. | | `files_changed_last_sync` | number or null | Files written by the last sync. `null` when `last_sync` uses the modified-time fallback. | | `drift_files` | number | Generated files that differ from what `sync` would write. | # Lint codes Run `agnostic-ai explain LINT011` for one code, or [lint](https://agnostic-ai.org/docs/cli-reference/check/#lint) to check your specs. An error fails `lint`; with `lint --strict`, any warning fails too. | Code | Finding | |------|---------| | LINT001 | Warning. A spec has no body and no description. Hook specs are skipped; see LINT052. | | LINT003 | Error. Two specs of one kind share a `name`. Hooks that share an event and matcher are fine. | | LINT004 | Warning. No enabled target supports the spec's kind, so sync writes it nowhere. The finding lists the tools that do. | | LINT005 | Warning. A hook sets `matcher` on an event that does not read one. Drop it or use a tool-call event such as `PreToolUse`. | | LINT006 | Error. Frontmatter opens with `---` and never closes. | | LINT007 | Warning, also printed by `sync`. A frontmatter key one edit away from a key agnostic-ai reads (`glob:` for `globs:`), so the setting is lost. Put tool-native keys under `x-:`. A key some tools read at the top level (Qoder's `glob:`, OpenCode's and Kilo's `mode:`) is flagged only when none of them is in `targets`. Settings and environment specs are not checked. | | LINT008 | Error. A stdio MCP server lacks `command:`, or an `http`/`sse`/`ws` one lacks `url:`. `x-` cannot set either. | | LINT009 | Warning. A permission that approves more than it says: `allow: Bash(git * main)` also matches a force push to main. Write the exact value, or keep `*` at the end (`Bash(go test:*)`). A `deny` rule of that shape blocks nothing in Claude Code, so it is flagged too. | | LINT010 | Error, `--global` only. A rule with scope, path, glob, or target conditions. | | LINT011 | Warning. Startup instructions exceed a word or byte budget. See [context budgets](#context-budgets). | | LINT012 | Warning. A skill or agent description (or `x-.description`) is longer than [`lint.description-chars`](https://agnostic-ai.org/docs/configuration/#lint) (default 1024). A skill description past 1024 warns even with a raised budget. | | LINT013 | Error, also from `validate`. A rule's `globs`, `paths`, or `x-.globs` is neither a string nor a list of strings, or `paths` and `globs` name different patterns. | | LINT014 | Error, `--global` only. A settings `effort` or `permissions.default-mode` that a tool's user settings cannot take, such as effort `max` for Claude or Copilot. `sync --global` drops it with a note. | | LINT015 | Warning. A spec body names another spec by a target-native path such as `.claude/skills/style/SKILL.md`. The finding names the `.agnostic-ai/` source path to use. | | LINT016 | Error. A `dev-commands` entry in an environment spec has no `name:`, has neither `command:` nor `url:`, repeats a name, is not a mapping, sets a key no tool reads, or gives `cwd`, `url`, `auto-port`, `port`, or `env` the wrong type. A `url:` entry with no `command:` is reported only when it reaches a tool that needs a command, such as Codex. | | LINT017 | Warning. A `gitignore.commit` entry such as `cursor:environments` names a target missing from `targets`. | | LINT018 | Warning. A skill's frontmatter sets `scope`, which does nothing; the folder under `skills/` sets the scope. | | LINT019 | Warning. A skill or command uses Claude Code body syntax (`` !`command` ``, a ` ```! ` block, `$ARGUMENTS`, `$0`, `$1`) outside a `::target` fence, so other enabled tools read it as text. See [Claude Code body syntax](https://agnostic-ai.org/docs/spec-format/skills/#claude-code-body-syntax). | | LINT020 | Warning. A rule sits in a folder that names a project directory, such as `rules/backend/`, but its `scope:` points elsewhere. `scope:` wins. Move the file or drop `scope`. | | LINT021 | Warning. With Codex enabled: a Bash `allow` or `deny` rule in portable Settings or `outputs.claude.settings.permissions` has no Codex prefix with the same decision. See [Bash permission translation](https://agnostic-ai.org/docs/targets/codex-reference/#translate-bash-permissions). | | LINT022 | Warning. A settings `protected` path covers a file sync writes, such as `.claude/**`. Protect the spec under `.agnostic-ai/` instead. See [protected paths](https://agnostic-ai.org/docs/spec-format/settings/#protected-paths). | | LINT023 | Error, and `sync` fails. A settings `protected` block has no `paths`, a `decision` other than `ask` or `deny`, an unknown key, or a path outside the project or with a character class, brace, or negation. | | LINT024 | Warning. A [`coverage.accept`](https://agnostic-ai.org/docs/configuration/#coverageaccept) entry matches no coverage note on one of its targets. Remove the entry or that target. | | LINT025 | Warning. A [`models`](https://agnostic-ai.org/docs/configuration/#models) tier a spec names has no entry and no `default` for an enabled tool that writes the spec, so that tool uses its default model. Also raised for a tier named like a Claude model, such as `opus`. | | LINT026 | Warning. A Claude model name reaches a tool that cannot load it, through an agent's `model` or the `default` of a tier it names. Write `model: {claude: }` or add the tool to the tier. | | LINT027 | Error, also from `validate`, and `sync` fails. An MCP spec holds a value JSON cannot hold, such as YAML `.nan` or `.inf`. The finding names the field, for example `x-amp.timeout`. | | LINT028 | Warning. An MCP `url` or `args` element holds a reference form only one tool reads, such as `${env:NAME}`, `{env:NAME}`, or `{% raw %}${{ secrets.NAME }}{% endraw %}`; the other tools get it as text. Write `${NAME}`, or move the value under `x-:`. | | LINT029 | Warning. With Kiro and a tool that writes rules into `AGENTS.md` (such as Codex) enabled, a Kiro agent sets `x-kiro.resources` without `file://AGENTS.md`. With Kiro's `chat.disableInheritingDefaultResources` on, that agent loads no always-on rules. Add `file://AGENTS.md` to its resources. | | LINT030 | Warning. A Gemini CLI hook command has a bare `$GEMINI_PROJECT_DIR`, `$GEMINI_CWD`, `$GEMINI_PLANS_DIR`, `$GEMINI_SESSION_ID`, or `$CLAUDE_PROJECT_DIR`, which Gemini replaces as text before the shell runs. Write `"${NAME}"`. | | LINT031 | Warning. A spec's `description` starts with `TODO`, the placeholder `agnostic-ai new` writes. It shows in tool pickers and decides when a skill fires. | | LINT032 | Error. A hook's `on:` or `match:` holds an unknown value, is mixed with `event:` or `matcher:`, names an event that an enabled tool it reaches does not read the same way (such as `on: stop` on Crush), or a tool kind that tool lacks (such as `match: read` on Codex). Sync leaves the hook out for that tool. See [portable events](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events). | | LINT033 | Error. A spec body holds an [agent or skill reference](https://agnostic-ai.org/docs/spec-format/#agent-and-skill-references) to a name that does not exist or does not sync to a tool the spec reaches. The finding lists the known names or the tools. | | LINT034 | Warning, project only. A hook's `event:` and `matcher:` have a portable form that gives every enabled tool the same native event and matcher, such as `event: PreToolUse` with `matcher: Bash`. The finding names the `on:` and `match:` to write; `agnostic-ai migrate --only hooks` rewrites them. | | LINT035 | Error, and `sync` stops. An MCP `env` or `headers` value is neither a `${NAME}` reference nor marked `!literal`, so it may be a secret sync writes into every tool. The finding names the server, field, and key, never the value. `agnostic-ai migrate --only secrets` turns a credential into a reference and marks the rest. For a server written into an agent file ([agent-only servers](https://agnostic-ai.org/docs/spec-format/mcps/#agent-only)), it also fires on a credential in `url`, `args`, an `x-` block, or a reference default. See [secrets and plain settings](https://agnostic-ai.org/docs/spec-format/mcps/#plain-settings). | | LINT036 | Error, also from `validate`, and `sync` stops. An agent's `can:` or skill's `allowed-tools` holds an unknown capability, a malformed `read()`, `edit()`, `shell()`, or `mcp:`, or is not a list; for an agent, also `can:` beside `tools:` or under `x-`. A settings permission rule that is an unknown capability, has a pattern on one that takes none (such as `write(.env)`), has an empty pattern, or is not a string (such as an unquoted rule holding `: `). See [capabilities](https://agnostic-ai.org/docs/spec-format/agents/#capabilities) and [permission rules](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules). | | LINT037 | Warning, only with `--suggest-capabilities`. A tool alias has an exact neutral capability form. | | LINT038 | Warning. A bare capability under settings `permissions.allow` or `permissions.ask` covers a whole tool. The finding names the tools that take it and a scoped alternative. Scoped and `deny` rules do not warn. See [permission rules](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules). | | LINT039 | Warning, project only, with the `memory` built-in. A [shared memory](https://agnostic-ai.org/docs/memory/) index has more than 100 lines, or the two indexes pass 6,000 bytes. When a target loads memory through the [session-start hook](https://agnostic-ai.org/docs/memory/#load-at-session-start), the finding names the first 10 facts that hook never loads; otherwise it names the targets that load the whole indexes every session. Merge or drop facts. | | LINT040 | Error, project only. A memory index line links a fact file that does not exist. Restore the file, or run [`memory index`](https://agnostic-ai.org/docs/cli-reference/memory/#memory) to drop the line. | | LINT041 | Warning, project only. A memory fact file has no index line, so no tool loads it. [`memory index`](https://agnostic-ai.org/docs/cli-reference/memory/#memory) adds one. | | LINT042 | Error, project only. A line in a memory index or fact file looks like a credential, such as a prefixed token, a URL password, or a `Bearer` token. The finding names the line, never the value. | | LINT043 | Warning, project only. A memory store no tool reads any longer holds facts personal memory lacks: Claude Code's own store once sync points Claude at personal memory, or the other personal folder after a `memory.personal` switch. The finding names the folder and the [`memory import`](https://agnostic-ai.org/docs/cli-reference/memory/#memory) command that copies the facts. It counts facts by file name, plus one for notes kept in that store's index, and never prints a fact. | | LINT044 | Error in project memory, warning in personal memory; not checked by `lint --global`. A memory index or fact line holds a character a reviewer cannot see, such as a zero-width character, a bidirectional control, or a Unicode tag. The finding names the line and code points, never the text. [`hook memory`](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-memory) leaves these characters out. A zero-width joiner or non-joiner between two non-ASCII characters, as in an emoji or a Persian word, is fine. | | LINT045 | Error. `session-retention-days` must be an integer of 1 or more. Remove the field to leave native retention unchanged. | | LINT046 | Warning. A spec writes one top-level key more than once, in frontmatter or in a YAML spec such as a hook or MCP server. Sync uses the last value; other YAML parsers reject the file. `import cursor` in 0.83.0 left a repeated `workspaces`. Remove the extra lines. | | LINT047 | Warning. With Claude Code enabled, a skill or command name holds a character Claude Code refuses, so it does not load: a control, zero-width, text-direction, or private-use character, `<` or `>`, or a blank other than one plain space between words. The finding names the code points. Rename the folder or file, or edit `name:`. | | LINT048 | Warning. An agent with a body has no `description`. Tools read it to decide when to hand the agent a task, and Claude Code skips an agent file without one, so sync writes the name there instead. Add a `description:` that says when to use the agent. | | LINT049 | Warning. An MCP spec holds what looks like a credential where LINT035 does not look: user info or a token in `url`, a token in `args`, an `env` or `headers` value in an `x-` block, or the default of a `${NAME:-default}` reference. The finding names the server and field, never the value. Write a `${NAME}` reference with no default; `!literal` plain settings pass. See [secrets and plain settings](https://agnostic-ai.org/docs/spec-format/mcps/#plain-settings). | | LINT050 | Error, and sync stops before writing any agent. An agent's `maxTurns` is not a whole number of 1 or more, or its `background` is not `true` or `false`. Values under `x-` are not checked. | | LINT051 | Warning, also printed by `sync`. A file under `.agnostic-ai/overlays/` is not one sync reads, so its keys reach no tool. A `.settings.*` file of a tool that merges `x-` from a [settings spec](https://agnostic-ai.org/docs/spec-format/settings/), such as `gemini.settings.json`, points there. Any other file, such as a misspelled `claude.setings.json`, gets the list of [overlays](https://agnostic-ai.org/docs/spec-format/overlays/) sync reads; a case-only mismatch is told to rename. Dot files, dot folders, and top-level README files are skipped. | | LINT052 | Error, also reported by `validate`. A hook spec has no `command`, no other handler such as `prompt` or `url`, at the top level or under an `x-` block, and no native form such as `x-kiro.action` or `x-gemini.hooks`, so no tool runs it. A key such as `run:` or `comand:` is named as the likely misspelling. | | LINT053 | Error. A tool in `targets` refuses a spec or setting, and sync stops for every tool. CodeBuddy takes agent names of lowercase letters and hyphens. Zed and OpenCode take skill names of 1 to 64 lowercase letters or digits with single hyphens, and Junie takes agent names that start with a lowercase letter followed by lowercase letters, digits, hyphens, or underscores, including commands and agents written as skills. Grok refuses rules while `gitignore.enabled` is on and `gitignore.commit` lacks `grok:instructions`. Rename the spec, or add `grok:instructions` to `gitignore.commit`. Each tool reports its first refusal. Tools outside `targets` and `lint --global` are not checked. | | LINT054 | Error, and sync stops before changing any tool settings. A settings spec's `voice` is not a mapping, or `voice.enabled` is not `true` or `false`, `voice.mode` is not `hold` or `tap`, `voice.language` is not text, or it holds another key. Set other native voice keys under `x-`. See [voice dictation](https://agnostic-ai.org/docs/spec-format/settings/#voice-dictation). | ## Context budgets LINT011 warns when a tool loads more words at session start than [`lint.instructions-words`](https://agnostic-ai.org/docs/configuration/#lint) allows (default 2000), counting the entry-point file as `sync` writes it, always-on rule files, skill and agent descriptions, and `@`-imported rule files. [`explain --context`](https://agnostic-ai.org/docs/cli-reference/inspect/#rank-context-contributions) ranks what adds the most. Tools with the same numbers share one line: ``` LINT011 [warn] AGENTS.md: cline, windsurf, trae load 2396 words every session: AGENTS.md 1187 (AGNOSTIC_AI.md 136, rules 1032), always-on rule files 908, skill descriptions 195, agent descriptions 106; budget 2000 (lint.instructions-words). ``` It also warns past a tool's own cap: | Tool | Cap | |------|-----| | Codex | Stops reading `AGENTS.md` at 32 KiB (`project_doc_max_bytes`). | | Antigravity | Truncates rule files past 24,000 bytes. | | Claude Code | Warns at startup when the combined agent descriptions pass 15,000 tokens. LINT011 estimates 4 characters per token and warns past the same figure. | ### LINT011 for the Codex AGENTS.md chain Codex also reads each scoped `AGENTS.md` between the root and its working directory, and stops at the same 32 KiB across the chain. LINT011 sums the chain per scope and warns past [`lint.codex-chain-bytes`](https://agnostic-ai.org/docs/configuration/#lint) (default 32768), naming the scope, the total, and each file with its size. A review spec with `target: cursor` is left out of Codex's `AGENTS.md`. # Inspect and preview Start with the item you know: `explain ` finds a spec's output, `why ` finds a generated file's source, and `render ` previews file contents. Use `compare` before changing tools and `graph` for an overview. ## explain List every output file and section one spec contributes to, read from the `` markers in merged documents. With `--file`, list the instructions that apply to one source file instead. Given an [error code](https://agnostic-ai.org/docs/errors/) such as `AAI-003` or a [lint code](https://agnostic-ai.org/docs/cli-reference/check/#lint) such as `LINT011`, print its cause and fix; a lint code adds its severity and any `lint` config key that tunes it (`severity` and `config` in JSON). ```bash agnostic-ai explain rules/conventional-commits.md --json agnostic-ai explain builtin:handoff --json ``` | Flag | Description | |------|-------------| | `--json` | Stable JSON for editor extensions and scripts. | | `--layers` | Show which project source layers replace or extend a spec. See [below](#debug-a-replaced-spec). | | `--global` | Explain a spec in `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`) or its `local/` layer. A relative path resolves against that root. | | `--file ` | Explain a source file. See [below](#explain-a-source-file). | | `--context` | Rank startup context. See [below](#rank-context-contributions). | | `--inputs` | List generator inputs. See [below](#list-generator-inputs). | Contributions are grouped by configured target, each tagged `(full file)` or `(section "")`, plus a "would emit if enabled" list for other tools. With `--global`, an agent or skill is a whole file, a rule a section of the instructions file, a hook its event in a shared hooks file or its standalone hook files, an MCP server its key in the user MCP file, and a settings spec each key it sets, tagged `(key "")`. Directory-based global hooks (Cline, Grok, and Kiro) also list copied command scripts, using the same paths and target root settings as `sync --global`. A spec that sets `model` or `effort` also lists its [tier](https://agnostic-ai.org/docs/spec-format/agents/#model-tiers) and the model and effort it resolves to per target; `tool default` means the target writes no model. A target with no key for it, such as Codex skills, drops it and `sync` prints a coverage note. For an agent's `can` or `tools`, and settings `permissions`, it lists each target's native names, unsupported rules included. A widening line names extra access a target grants (for Codex, from the merged permissions and any explicit command policy source); `explain` shows it even with `on-unsupported: error`, where `sync` fails. Tool-specific overrides show their values and the field that wins. ### Read explain JSON ```json {"version": "1", "command": "explain", "spec": {"kind": "rule", "name": "...", "path": "..."}, "contributions": [{"target": "...", "path": "...", "section": "...", "mode": "full|section|key"}], "would_emit_if_enabled": [], "model_tier": "strong", "models": [{"target": "...", "model": "...", "effort": "..."}]} ``` `model_tier` and `models` are omitted when the spec sets neither `model` nor `effort`. The `capabilities` array, omitted for specs without capability fields, holds `target`, `field`, `capability`, `native`, and `supported`; `widening` lists extra access when present, and `override` names a native field or policy source that replaces the portable rule. ### Claude skill overrides For a skill sync writes for Claude Code, `explain` lists each Claude settings file whose `skillOverrides` names the skill, highest precedence first. It reads the same files and applies the same rules as [`doctor`](https://agnostic-ai.org/docs/cli-reference/check/#claude-skill-overrides), including how rejected values, duplicate keys, and files that are not strict JSON show. A line that does not take effect is marked, so the first unmarked line is the value Claude Code applies: ```text claude overrides: .claude/settings.local.json: off (Claude Code refuses to run it; change it in /skills) ~/.claude/settings.json: name-only (not applied: a higher file sets it) ``` The block is omitted when no file names the skill and every file reads. `--json` adds the same entries under `overrides`, with `rejected`, `conflict`, `uncertain`, or `problem` as in doctor: ```json "overrides": [{"target": "claude", "path": ".claude/settings.local.json", "scope": "local", "value": "off", "effective": true}] ``` ### Debug a replaced spec When editing a pack rule has no effect, inspect its source layers: ```bash agnostic-ai explain rules/conventional-commits.md --layers ``` ```text rule conventional-commits .agnostic-ai/packs/team/rules/conventional-commits.md [pack:team] base (replaced) rules/conventional-commits.md [project] replace (contributor) .agnostic-ai/local/rules/conventional-commits.md [project-user] extend (contributor) effective source: .agnostic-ai/local/rules/conventional-commits.md ``` Edit either contributor to change the effective spec. Passing the replaced pack path gives the same report. Sources follow sync's order: enabled built-ins, installed packs, project specs, then project-local overrides. `base` starts a spec, `replace` discards the earlier chain, and `extend` merges with it. `contributor` means the source survives resolution; it does not attribute fields or body text, which an extension can replace. With `--json`, `spec` is the effective source and `sources` lists each candidate's `source`, `operation`, and final `role`. Built-ins show a symbolic `builtin` name and version, not a cache path. The view reads installed sources offline, supports project specs only, and cannot combine with the other explain views or `--target`. ### Explain a source file List the instructions planned for one project file, with their source, output path, file or directory condition, and reason. Claude Code, Codex, and Cursor are supported. ```bash agnostic-ai explain --file services/payments/handler.go --target cursor ``` | Flag | Description | |------|-------------| | `--file ` | Project file to inspect. It does not have to exist. Cannot combine with a spec or code argument. | | `--target ` | Required with `--file`. A configured target: `cursor`, `claude`, or `codex`. | The report reads planned sync output, not what the model loaded. It does not see user instructions, the session launch directory, import approval, or session settings that turn off project instructions. ### Read instruction statuses, native discovery rules, and JSON | Status | Meaning | |--------|---------| | `always` | No file condition: Cursor's `alwaysApply: true` or root `AGENTS.md`; Claude Code's project `CLAUDE.md` or rules without `paths`; Codex's root native instruction file. | | `match` | A file pattern or a nested instruction directory covers the file. | | `no-match` | A selector misses the file, or a planned instruction filename takes priority over this output. | | `model-selected` | `alwaysApply: false` with a description and no globs. Cursor's agent decides. | | `manual` | `alwaysApply: false` with neither. Loads only when `@`-mentioned. | | `unknown` | The command cannot establish discovery or evaluate the selector. The reason names the missing information. | | `excluded` | Target selection (`target`, `targets`, `target-exclude`) leaves the target out. | | `not-emitted` | The rule selects the target but sync writes no instruction for it. | | Tool | Discovery the report models | |------|-----------------------------| | Cursor | A root `AGENTS.md` written for another tool reaches Cursor too. Braces, character classes, negation, or unreadable frontmatter report `unknown`. | | Codex | Root `AGENTS.md` and `AGENTS.override.md` have no file condition; in one directory the override wins. Nested instructions report `unknown` and say whether the file is under their directory, since [Codex discovers](https://learn.chatgpt.com/docs/agent-configuration/agents-md) from the root to the session launch directory. Custom filenames report `unknown` because they need a fallback filename or session setting. | | Claude Code | Reads generated `paths`, including tool-specific overrides, and Markdown rules under `.claude/rules/`. A nested `CLAUDE.md` covers files beneath it; elsewhere it depends on the session start directory, so `unknown`. Imports and outputs outside native discovery that need session approval or settings, and patterns it cannot evaluate (braces, character classes), report `unknown` with the reason. See [Claude Code's instruction discovery](https://code.claude.com/docs/en/memory). | ```json {"version": "1", "command": "explain", "file": "...", "target": "cursor", "note": "...", "instructions": [{"status": "match", "source": "...", "output": "...", "selector": "...", "reason": "..."}]} ``` ### Rank context contributions `--context --target ` ranks the sources that add the most text at session start, counting words like `lint` does, with bytes. It needs a configured target, takes no spec argument, and writes nothing. ```bash agnostic-ai explain --context --target claude --json agnostic-ai explain --context --target cursor --file src/main.go ``` Counts are estimates from planned output and spec text, not model tokens; runtime context, external imports, user-owned files, and host truncation are not counted. Inlined imports keep their source paths; unresolved ones are not read. The report ends with opportunities: paragraphs of at least eight words written more than once in the specs that load at startup for that target, byte for byte after trimming. Each lists its copies, source files, size per copy, and the words and bytes one session start could drop at least. A spec that two generated files both carry counts once, because the fix is the tool setting that reads both files, not the spec. Similar wording is not reported, because only you can tell whether two instructions mean the same. Review the copies and edit the canonical specs, then run the command again to see the new counts. The command never edits a file. ### Read context accounting groups and JSON Entry-point layers, marked rule and review sections, inlined imports, always-on rules, each skill and agent description, and generated framing count separately, so the startup total matches lint. Sources sharing a document count once. Skill and agent bodies and conditional rule bodies appear under `on-demand`, outside the startup total, with their own `on_demand` sum. With `--file` for Cursor or Claude Code, matching scoped rule bodies appear under `file-scope` with their own total; unmatched or uncertain ones stay on demand. Entries sort by group, then words and bytes (descending), then source path and category. Text and JSON use the same numbers. ```json {"version": "1", "command": "explain", "target": "claude", "note": "...", "startup": {"words": 80, "bytes": 500}, "file_scope": {"words": 0, "bytes": 0}, "on_demand": {"words": 300, "bytes": 2000}, "contributions": [{"source": ".agnostic-ai/skills/review/SKILL.md", "category": "skill discovery", "load": "startup", "words": 40, "bytes": 220}], "opportunities": [{"kind": "duplicate-passage", "target": "claude", "load": "startup", "passage": "...", "copies": 2, "size": {"words": 14, "bytes": 74}, "reduction": {"words": 14, "bytes": 74}, "sources": [{"source": ".agnostic-ai/AGNOSTIC_AI.md", "category": "entry-point layer"}], "suggestion": "..."}]} ``` `file` is present when requested. `load` is `startup`, `file-scope`, or `on-demand`. `on_demand` and `opportunities` were added in version `1` without changing other fields; `opportunities` is an empty array when there are none, largest `reduction` first. `size` is one copy; `reduction` is the other copies. ### List generator inputs ```bash agnostic-ai explain --inputs ``` List the files and directories that can affect generated output, one per line relative to the repository root (`--json` for an array). Takes no spec argument or `--file`. Used by [git hooks](https://agnostic-ai.org/docs/git-hooks/#check-staged-files). It lists the config files, `.agnostic-ai/**`, source directories outside `.agnostic-ai/`, files reviews pull in with `@path` (and the entry point's, with `sync.resolve-imports: inline`), `agnostic.packs.lock`, `.gitignore`, and `builtin:@` for each enabled built-in. ## compare Compare how two built-in targets handle the project's agent and skill fields, rule activation, hooks, MCP connections, and portable permissions, before you switch or add a tool. The [compare targets](https://agnostic-ai.org/docs/compare/) page shows sample paths for any pair. ```bash agnostic-ai compare claude cursor ``` | Flag | Description | |------|-------------| | `--json` | Stable JSON for scripts. | Each field gets one result per target, with the output paths or the reason, and a `next:` step when known, such as a required `outputs.zed.tasks-file`. `(differs)` marks a field whose results differ. | Result | Meaning | |---|---| | `preserved` | Written under the same key with the same values. This describes the file, not that the tools behave the same, a hook ran, or an MCP server connects. | | `translated` | Written under another key or file, with rewritten values, or only in part. | | `unsupported` | The target has no place for the field or the kind. A field the tool ignores keeps that reason even when its native key is written. | | `excluded` | The spec never reaches the target: a target filter, an opt-in output, or a scope the target cannot express. | | `unknown` | The written files do not show either way. | The command fails on unknown targets, the same target twice, invalid specs or config, and external adapters. ### Read compared fields, permission mappings, and JSON It covers agent and skill fields; rule `scope`, `paths`, `globs`, and `alwaysApply`; hook `on`, `match`, `event`, `matcher`, `command`, `args`, `timeout`, and `failClosed`, using current event mappings and written handlers; MCP transport (`type`), `command`, `args`, `url`, and each `env` and `headers` entry; and portable settings permissions one source rule at a time (allow, ask, deny, default-mode). Other settings, kinds, and hook fields are out of scope. Skill fields such as `argument-hint`, `effort`, and `disable-model-invocation` are judged from the files each adapter writes, including Codex's separate policy files. MCP results never show command, argument, URL, environment, or header values, defaults, or reference text. A server whose URL or argument reference the tool cannot represent may be omitted whole; no explicit transport gives `unknown`. Permission rows use the source index, such as `permissions.deny[0]`, name the rule, and show native override values and partial mappings. A source or native rule that holds a credential shows as its tool name around ``, the marker `sync --global` prints, such as `shell()` or `prefix_rule()`; in an `x-` override, only the command pattern or string that holds it is replaced. Recognized credentials in complete native permission records, such as Augment webhook policies, are hidden in text and JSON summaries but compared as written; filename patterns and ordinary permission actions stay visible. Codex shell permissions need `outputs.codex.exec-policies-from-permissions: true`. Default-mode applies only to global sync, so project comparison reports it unsupported. A difference in effective decision or partial support counts, even when both results are translated. ```json {"version": "1", "command": "compare", "targets": ["claude", "cursor"], "coverage": "...", "caveat": "...", "specs": [{"kind": "agent", "name": "...", "path": "...", "fields": [{"field": "tools", "differs": true, "results": [{"target": "cursor", "status": "unsupported", "reason": "...", "next": "..."}]}]}], "fields": 6, "differences": 3} ``` ## render Print what each target writes for one spec, without writing files. ```bash agnostic-ai render rules/no-console-log.md --target claude,codex ``` | Flag | Description | |------|-------------| | `-t, --target ` | Targets to render, repeated or comma-separated. Default: all in `agnostic-ai.yaml`. | Each file prints as `# target: : ` and its body. A target that writes nothing for that kind prints a note. Known credential values are hidden; see [credential previews](https://agnostic-ai.org/docs/cli-reference/sync/#credential-previews). ## graph Show which targets and files each spec feeds (spec → target → file). Read-only. When a spec seems missing from a tool, run `agnostic-ai graph --spec `: a `-` means that tool gets no file, so check the tool's supported kinds and the spec's target filters. Rows sort by spec name, then target. ```text $ agnostic-ai graph spec | claude cursor codex code-reviewer | agent agent agent no-console-log | rule rule rule ``` `mermaid` starts with `graph LR`, `dot` pipes to Graphviz (`agnostic-ai graph --format dot | dot -Tsvg > graph.svg`), and `json` gives one record per file: ```json {"spec": "no-console-log", "kind": "rule", "target": "claude", "path": ".claude/rules/no-console-log.md"} ``` | Flag | Description | |------|-------------| | `--format` | `text` (default, aligned table), `mermaid`, `dot`, `json`. | | `--target` | Restrict to one configured target. Any other name fails. | | `--spec` | Restrict to one spec name. | | `--kind` | Restrict to one kind: agent, skill, rule, hook, mcp, command, settings, review, environment, ignore. Any other kind fails. | ## why Name the source of a generated file, so you edit the source instead. Use a project-relative path, even for a file not synced yet; symlinks resolve to the real path. In VS Code, `agnostic-ai: Open canonical source` does the same for the current file. ```sh $ agnostic-ai why .cursor/rules/no-console-log.mdc .cursor/rules/no-console-log.mdc adapter: cursor output keys: (adapter defaults) last sync: 2026-05-15T10:23:00Z sources: [rule] no-console-log (.agnostic-ai/rules/no-console-log.md): full ``` Edit the path under `sources`, then run `agnostic-ai sync`. `why` can recognize files from tools outside your `targets` list. If one of those tools cannot generate files with your configuration, it skips that tool. A failure from a tool in `targets` stops the command and names that tool once. | Field | Meaning | |-------|---------| | `adapter` | Tool that wrote the file. For a shared file such as `AGENTS.md`, the first tool that reads it. For a path several tools share (`.agents/skills/`), a tool listed in `targets`. If only a tool you did not list writes the path, it reads `adapter: amp (not configured)`. | | `output keys` | Every `outputs..*` key whose value appears in the path, or `(adapter defaults)`. | | `last sync` | UTC time from `.agnostic-ai/.sync-state`, or `unknown` when that file is missing. | | `configured` | JSON only. `true` when the tool is listed in `targets`. | | `sources` | Every spec that feeds the file: `full` when the spec owns the file, `section` when it is one of several merged into a shared file. Root instruction files list `.agnostic-ai/AGNOSTIC_AI.md` first, then `.agnostic-ai/local/AGNOSTIC_AI.md` when present, then each [inlined rule](https://agnostic-ai.org/docs/target-behavior/#inlined-rules) as a `section`. | Without `.agnostic-ai/.sync-state`, run `sync` first. A spec path points you to `explain`, and `AGNOSTIC_AI.md` names the entry-point files it feeds; a path sync does not write reports "not synced or not tracked". | Flag | Description | |------|-------------| | `--format` | `text` (default) or `json`. | | `--json` | Alias for JSON output. | ## view Open the bundled web interface in your browser; `ui` is an alias. From a project folder or any folder inside it, `view` reads that project. Outside a project, it reads the user configuration in `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`). Keep the terminal running; Ctrl-C stops it. See the [browser walkthrough](https://agnostic-ai.org/docs/inspect-configuration/). ```bash agnostic-ai ui agnostic-ai view --json agnostic-ai view --global ``` | Flag | Description | |------|-------------| | `--json` | Print the overview as JSON (schema `overview/v1`) without starting a server or opening a browser. | | `--dir` | Read this project directory instead of discovering one. | | `--addr` | Explicit loopback IP and port. Default `127.0.0.1:4310`. When set, `view` stays on this port and fails if another configuration serves it. | | `--global` | Read the user configuration instead of a project. Cannot combine with `--dir`. | A second run for the same project and scope opens the running server and exits. When the default port serves another project or scope, an `--api-only` server, or another program, `view` takes the next free loopback port up to `127.0.0.1:4319` and prints it; later runs find that server. An invalid project configuration reports an error. The overview lists context, authority, tools, lifecycle, and settings, with findings for the selected targets; the JSON overview also compares generated files. Specs scoped only to other targets are left out of lists and counts. Search within a section to find a source file or value, or press **Search** (`Cmd+K` or `Ctrl+K`) to search every section, findings included. Opening a result puts a link to that entry in the address bar; it survives a reload. The browser has light and dark themes and works on narrow screens. Other browser features are under [serve](#serve). ## serve Serve the bundled web interface and a local JSON API. It works offline without a source checkout or frontend build, and reloads configuration on every request. ```bash agnostic-ai serve --global agnostic-ai serve --api-only ``` | Flag | Description | |------|-------------| | `--addr` | Explicit loopback IP and port. Default `127.0.0.1:4310`. | | `--allow-origin` | Additional exact browser origin, such as `http://127.0.0.1:5173` for Vite. The server accepts loopback hosts and same-origin requests otherwise. | | `--dir` | Project directory to read. Default: the current directory. | | `--ui` | Serve a built client directory, such as `ui/dist`, instead of the bundled one. | | `--api-only` | Serve only the JSON API. Cannot combine with `--ui` or `--open`. | | `--open` | Open the served URL in a browser. | | `--global` | Serve the user configuration. Its overview shows `lint --global` findings and drift against what `sync --global` would write; doctor checks are skipped. | | `--no-installed` | Skip comparing generated files with disk. | In the browser: - **Docs** beside each section heading opens the matching spec or reference page in a new tab, including empty sections. - **Authority** lists permission files and rules. **Add rules** or **Edit rules** opens an editor for Allow, Ask first, or Always blocked; preview the source-file change before saving. See [permission editing](#permission-editing). - **Tools** lists the MCP server specs. Under **Found in a tool's own config**, it also lists servers a configured tool keeps in its own MCP file that no spec of the same name covers, such as one you signed in to from inside Claude Code. Each row names the server, the tool or tools that hold it, the file (`~/.claude.json`, `.mcp.json`), and the transport, with the `agnostic-ai import ` command that turns it into a spec (`import --global ` for a user file). A project page reads each tool's project and user files, plus Claude Code's servers for this project under `projects` in `~/.claude.json`; `--global` reads user files only. A row has no command when no import reads its file, such as Claude Code's per-project servers or an `outputs` MCP file path the tool's import does not follow. It shows no command, argument, URL, environment, or header value, and a file it cannot read adds no row. The JSON overview carries these rows as `native` on the Tools section. - **View and edit** below the Permissions, Built-ins, or Preferences heading switches Shared and Personal values. **Save new specs for** above the native server list chooses Shared or Personal for **Make it a spec**. - **Make it a spec** on a native-only server previews one spec file, its contents, the variables you must set, and the tools it will reach after sync. **Apply** saves the spec using the [MCP import rules](https://agnostic-ai.org/docs/spec-format/mcp-import/), then asks you to run `agnostic-ai sync` (or `sync --global`). Shared saves use the project's configured MCP source folder (`.agnostic-ai/mcps/` by default), or `mcps/` in the global home; personal saves use `.agnostic-ai/local/mcps/`, or `local/mcps/` in the home. An existing spec name or a credential import cannot rewrite stops the save. Tool files stay as they are, including the original server; each tool signs in on its own. Snapshots have no action. - The header labels the page **Project** or **Global**. On a project page, **Open global settings** opens the user configuration in a new tab: it reuses a `serve --global` already running on `127.0.0.1:4310` to `4319` that started in this project, or starts one on the next free port. A global page that started in a project (from that button, or `ui --global` or `serve --global` run in a project folder) shows **Open project** with the project's name to go back. A server the button starts stops with the server that started it, and its way back returns to that server, also when it runs on another `--addr`. Each tab edits only the configuration it serves. - **Open files in** in the header picks Cursor or VS Code for every **Open** link, and the browser remembers it. Choose an editor installed on your machine, then allow the browser to launch it when prompted. If nothing opens, choose another editor or copy the path. A finding on a file opens at its line when known. **Copy path** sits beside each link, because the browser cannot tell whether the editor opened. - All five page links remain visible on narrow screens. The phone header keeps the configuration switch beside an Editor menu. **Context**, **Tools**, and **Settings** use tabs for their source groups. Tools separates configured servers, servers found in native configs, and environments; Settings separates built-ins, preferences, and config files. Config files lists the source files, then shows their combined effective values once. Tabs support arrow, Home, and End keys. On phones, Context and Tools tabs scroll horizontally; Settings keeps its three tabs visible. The selected tab survives refresh and browser history. Switching tabs keeps unsaved input and previews. Search covers all source tabs and opens a matching group; linked results open their owning tab. Authority keeps permissions together, and Lifecycle keeps its session timeline. - With `--global`, the `AGNOSTIC_AI.md` row in **Context** has **Copy refine prompt**: a prompt that asks a coding agent to review the file against current guidance for instruction files, propose a full rewrite with a change table, recommend which rules belong in an existing or new skill or agent, and leave the file unchanged until you approve. - **Context size** in **Context** shows the [context report](#rank-context-contributions) for a chosen target: startup, file-scoped (for a file you enter), and on-demand sizes, ranked sources, and duplicated startup passages. Each duplicate has edit links to its sources and **Copy review prompt**, which asks a coding agent to compare the copies and propose a change with its tradeoffs before editing. **Refresh** reads the specs again. Switching coding tools keeps the report space while new values load. Sizes are words and bytes, not tokens, and smaller numbers do not guarantee lower cost or faster replies. - Each finding has **Copy fix prompt**; **Details** shows the suggested fix, explanation, and full prompt. After you fix the source, **Recheck** runs the checks again with the same refresh and keeps your page, search, and unsaved input. It says whether that finding still appears, marks new findings from the same check on the same file, and treats a failed refresh or a lint run that stopped early as unknown, never as fixed. A doctor finding that disappears is shown as not confirmed, because these doctor checks skip files they cannot read. Snapshots cannot recheck. A findings summary opens the folded warnings below the controls; errors stay visible above them. - Tool lists use logos, with the full name on hover. Source files appear in compact lists with paths and edit links. Every lifecycle stage starts closed and shows when it runs and its hook count. Open a stage for its tools and hooks. **Expand all** and **Collapse all** control every stage on the page. Search opens matching stages; clearing it closes them. A linked hook or finding opens its stage and receives focus. Open states are not saved. Native settings are grouped by tool, with the `x-` prefix shown once. - **Settings** shows configured fields first and the rest under **Unconfigured fields**, including a default model or reasoning effort. Global scalar fields offer **Update** and **Clear**. Session retention and voice dictation are editable only with `--global`. Voice has an on/off switch, a hold or tap menu, and a spoken language field. - Default model and reasoning effort have **What each tool gets**: the value sync writes for each configured tool, the file or `agnostic-ai.yaml` key it comes from (Claude Code `outputs.claude.settings.model`, Codex `outputs.codex.config`), and why each other value loses: a later spec, a personal override or clear, a spec scoped to other tools, or a per-tool map with no entry. It names the model tier and alias, says when a tool has no place for the value, and says when sync writes none, without guessing the tool's own default. It reports whether installed files match, not what a running tool uses. It points out an `x-.model` native key; other `x-` keys and imported native settings files are not covered. The JSON overview carries it as `resolved` on those fields. - Under **What each tool gets**, each tool has its own edit control. It saves `model.` or `effort.` in the chosen file, which wins over the value for every tool. The origin line says where the value comes from, such as `personal · codex` or `shared · all tools`, and names the `agnostic-ai.yaml` key that replaces it when one does. **Use the value for every tool** removes that tool's entry. Effort has a control only for tools that take an effort setting. A spec whose `model` names a model tier stays read-only here, and so does one tool's effort when the tier gives the effort. - In a project, each built-in switch previews the config edit, generated files, and findings for every target. **Apply change** and **Cancel** stay at the top. Tools, files, and review notes start collapsed; notes group by tool in a bounded list, with **Copy notes** for the full messages. Pending source changes remain visible. **Apply change** writes the change; **Cancel** discards the preview, so a reload does not bring it back. **Undo last change** restores the files and permissions from before the last apply. Undo survives a browser reload, expires when the server stops, and is replaced by the next apply. Turning on `memory` in a Git checkout also adds a `.git/info/exclude` line so Git ignores Claude Code's private `settings.local.json`. - In a project, a default model or reasoning effort uses **Preview and apply**, **Preview removal**, and **Preview personal reset**. The preview names the settings file, the targets, and every source and generated file that changes, without writing any of them. **Apply change** writes them and checks the generated files; that does not mean a running tool has reloaded them. Built-ins and settings share one **Undo last change**, which names the change. - Global built-ins and settings edits save source files without syncing. Run `agnostic-ai sync` (or `sync --global`) to apply them. If a boolean or built-in changed since the page loaded, the save stops and offers **Reload saved values**. Apply needs no earlier sync: the preview lists the generated files, sync state, and lock `.gitignore` entries it creates. It includes pending source changes when files are missing or match sync's recorded checksums, and undo can restore them. These need the CLI: hand edits, files without ownership records, external adapters, paths outside the project, linked skill folders, and memory changes that need fact migration or external storage. A changed input or output blocks apply or undo until a fresh preview. A failed write restores the files the operation owns; a recovery error names any it kept and points to `agnostic-ai doctor`. Edits keep unrelated text and private file permissions. A validation failure restores the edit, unless another process changed the file; that is reported as a conflict. ### Save shared or personal changes {#edit-scope} **View and edit** at the top of **Settings** and **Authority** selects both the values shown and where edits go; the browser remembers your choice. **Shared (committed)**, the default, shows shared built-in states and settings, and writes files Git tracks. **Personal (not committed)** shows your overrides, with inherited shared settings labeled, and writes git-ignored [local overrides](https://agnostic-ai.org/docs/local-overrides/): built-ins go to `agnostic-ai.local.yaml`, and a settings field to `.agnostic-ai/local/settings/.yaml`, which overlays the shared spec of that name. In the global view they go to `local/agnostic-ai.yaml` and `local/settings/.yaml`. Outputs a personal built-in adds go to `.git/info/exclude`, not the shared `.gitignore` block. The config snapshot and **What each tool gets** still describe the combined configuration used for sync. A value from a personal file shows a **personal** badge in Personal. Personal values take priority for you; rows where they differ show both values, such as **Shared: Off · Personal: On**. Built-in counts follow the selected view. A shared control blocked by a personal override shows its shared value, stays disabled, and points to **View personal settings**. In Personal, **Reset to shared value**, or **Preview personal reset** for a project model or effort, removes your override and the personal file once it is empty. Your first personal built-in change copies the shared list into `agnostic-ai.local.yaml`, since that list replaces the shared one whole; **Reset built-ins to the shared list** drops it. A personal list set through a YAML merge (`<<`) is refused; edit it by hand. A personal save is refused when its file resolves, through links, outside the local folder, into a shared config or source folder, or to a file Git tracks. Before a personal save writes its file, the server checks that Git ignores it. When no rule does yet, as in a project that never ran `sync`, it adds the local folder (or `agnostic-ai.local.yaml`) to `.git/info/exclude`, which linked worktrees share. Your other lines in that file stay. If Git still would not ignore the file, the save is refused and the personal file is not written; add the path to `.gitignore` and save again. A new `.git/info/exclude` rule remains if it was added before a refusal or stale conflict, to protect personal files. Outside a Git repository there is nothing to ignore, so the save writes the file directly. In a linked worktree, a built-in only you turn on needs `agnostic-ai sync` in the CLI. The first personal change keeps the `memory.personal` mode inherited from the main checkout, and a reset leaves `memory.personal: checkout` behind rather than switch to the main checkout's mode. ### Edit permission rules {#permission-editing} In **Authority**, select Shared or Personal, then **Add rules** or **Edit rules** on Allow, Ask first, or Always blocked. Enter one [permission rule](https://agnostic-ai.org/docs/spec-format/settings/#permission-rules) per line, such as `shell(git status)` or `read(src/**)`. Blank lines are ignored. **Preview changes** shows the file, scope, and exact diff. **Save rules** saves only that source file; run the displayed `agnostic-ai sync` or `agnostic-ai sync --global` command to apply it. The support table names which tools accept permissions. Default mode and protected paths remain read-only. A personal edit starts with the shared rules and replaces only the selected list for that spec; other settings keep inheriting. A changed personal list offers **Compare shared and personal**. **Reset to shared rules** previews removing the override. An empty list is an explicit override with no rules; resetting removes it and inherits Shared again. Shared editing stays blocked while a personal list replaces it. If a personal spec clears the whole group with `permissions: null`, individual lists stay read-only in both views. Shared still shows its baseline; Personal shows the clear and names the file to edit. Remove that whole-group clear in the personal file before editing one list. When several settings files are editable, choose one explicitly; lists from other specs still contribute their rules. With no settings spec, the editor creates `settings/permissions.yaml` under the configured source root, or `local/settings/permissions.yaml` for Personal. Pack-only sources remain read-only. Invalid rules, unsafe file paths, and credentials are refused. A changed file stops saving and offers **Reload saved values**, which keeps your draft for a fresh preview. Permission saves have no browser undo. ### API Edit and impact requests take JSON. Every edit and preview names its `audience`: `shared` or `personal`. Except for `/api/edit/mcp` and `/api/edit/permissions`, the older `scope` field (`team` or `local`) is accepted until the next minor release; a request that sends both with different meanings returns 400. A change plan carries both `audience` and `scope` until then. A boolean edit must include its `enabled` or `value` field, even when false. Invalid values and YAML that cannot be spliced safely are refused without changing the file. Previews hide recognized credentials using the [credential preview rules](https://agnostic-ai.org/docs/cli-reference/sync/#credential-previews), and run no MCP servers, hooks, or external adapters. | Endpoint | Body or query | |----------|---------------| | `GET /api/health` | None. Returns `ok`, absolute `root`, `global`, and `ui` (whether the web interface is served), without loading specs. A global server started in a project adds that project's absolute `projectRoot`. | | `POST /api/switch` | `{}` with `Content-Type: application/json`. Finds or starts the server for the other configuration: the user configuration from a project, or `projectRoot` from a global server. Returns `url`, `root`, `global`, and `started`. Answers 404 on a global server with no `projectRoot`, and 503 with the command to run when no server could start. | | `GET /api/views/overview` | None. | | `GET /api/views/context` | `?target=`, a configured target; default the first one. `&file=` adds scoped rules for that file, as `--file` does; `fileTargets` lists the targets that accept it. Returns `targets`, `target`, absolute `root`, and `report`, the `explain --context --json` output. With `--global`, `supported` is false and `reason` says why. | | `GET /api/views/authority` | `?agentId=&target=` for per-agent decisions. | | `POST /api/views/authority/impact` | A permission proposal to preview. | | `GET /api/views/authority/schema` | None. | | `POST /api/edit/builtin` | `{audience, name, enabled, expected?}`, or `{audience: "personal", reset: true, expected?}` to drop the personal list. `expected` is the shown boolean, or the shown list for reset; a changed value returns 409 without saving the requested change. Built-ins apply to every tool, so a `target` other than `all` returns 400. | | `POST /api/edit/value` | `{audience, path, key, value, expected?}`, where `value` is a boolean, or `null` with `audience: personal` to reset. `expected` is the shown boolean; a changed value returns 409 without saving the requested change. The key already names the tool (`x-claude.verbose`), so a `target` other than `all` returns 400. | | `POST /api/edit/mcp` | `{action, audience, name, target, path, scope, expected?}`. `name`, `target`, `path`, and `scope` identify one native row from the overview; here `scope` is its source scope (`project` or `user`). `action: "preview"` returns `file`, `spec`, `variables`, `targets`, `syncCommand`, and `expected`. `action: "apply"` requires that `expected`, writes only the spec, and returns those fields with `ok: true`. A changed preview or existing spec stops apply; a credential import cannot rewrite is refused with its import message. Personal saves use the same Git-ignore check as other edits. | | `POST /api/edit/permissions` | `{action, audience, path?, key, rules, before?, expected?}`. `key` is `permissions.allow`, `permissions.ask`, or `permissions.deny`; `rules` is a string list, or `null` to remove that list. `before` is the selected audience's own list as shown, or `null` when absent; a mismatch returns 409. Preview returns `schemaVersion: "permission-edit/v1"`, `file`, `key`, `audience`, `before`, `rules`, `diff`, `expected`, and `syncCommand`. Apply requires the preview's `expected`, rechecks its inputs, saves only the source file, and returns the preview fields with `ok: true`. Personal saves use the Git-ignore check. | | `POST /api/edit/field` | `{audience, path?, key, target?, value, expected?}` for `model`, `effort`, and global `session-retention-days`, `voice.enabled`, `voice.mode`, or `voice.language`. `target` is `all` (the default) or a configured tool; for one tool the edit writes `model.` or `effort.`, turning `model: x` into `{default: x, : value}`, and an effort level is checked against that tool only. A tool with no effort setting returns 400. When a model tier gives the spec its effort, one tool's effort returns 422, because a spec value turns the tier's effort off for every tool. Session retention takes only `all`. With `all`, a field that is already a per-tool map gets its `default` entry. `null` clears the value at that address, or resets your override with `audience: personal`; a map left with only `default` goes back to a single value, except in a personal file over a shared per-tool map, where a single value would replace the whole shared map, so it stays `{default: x}`. `expected` is the value the page showed at that address in the file being written, or `null` if that file did not set it. If the file now holds another value, or a personal file now sets that address for a shared save, the answer is 409 and nothing is written. A file that already holds the new value counts as saved. Voice values are a boolean for `voice.enabled`, `hold` or `tap` for `voice.mode`, and a non-empty string for `voice.language`; only the global view and target `all` are accepted. `null` clears one member, removing an empty `voice` map. | | `GET /api/changes/builtin` | None. Returns the session's change state. | | `POST /api/changes/builtin/preview` | `{audience, name, enabled}` for any shipped built-in, `{audience: "personal", reset: true}`, or `{audience, setting: {path?, key, target?, value, expected?}}` for `model` or `effort`, with the fields `/api/edit/field` takes. A setting plan carries `key`, `target` for one tool, and a `title` such as `effort set to high` or `model for codex set to gpt-5.5`; undo state names it in `undoTitle`. | | `POST /api/changes/builtin/apply`, `.../undo`, `.../cancel` | `{id}` from the change state. Cancel discards that preview without writing files and keeps the last undo; it answers 409 when that preview was already replaced or closed. | Omit `path` only when at most one editable settings spec exists; with none, a field save creates `settings/defaults.yaml`, or a permission save creates `settings/permissions.yaml`, for the selected audience. The response names the edited file. ## mcp tools Connect to each configured MCP server, list its tools, and cache the result so `view` and `serve` show them without connecting. It is the only command that connects to MCP servers. ```bash agnostic-ai mcp tools agnostic-ai mcp tools --global ``` | Flag | Description | |------|-------------| | `--dir` | Project directory to read. Default: the current directory. | | `--global` | Read the user configuration in `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`). | | `--json` | Print the cached result as JSON. | | `--timeout` | Maximum probe time per server, including its header helper. Default `20s`. Process cleanup is bounded separately. | Stdio servers start with their `command`, `args`, and `env`, with relative or omitted working directories resolved against the configuration root; HTTP servers use their headers and optional `headersHelper`. `${NAME}` resolves from the environment and `${workspaceFolder}` is the root; an unset reference is reported without connecting. SSE, WebSocket, and disabled servers are not probed. Up to four servers run at once; cancellation stops owned processes. Errors and tool descriptions are redacted before display or caching. The cache lives in the user cache directory, shared across directory aliases. Changed specs mark a listing stale; rerun `mcp tools` to show it in the overview. # Maintain and set up This page covers handoffs, generated-file recovery, packs, hook helpers, git hooks, completion, and the language server. Other maintenance commands have their own pages: [project CLI and readiness](https://agnostic-ai.org/docs/cli-reference/project/), [upgrade and migrate](https://agnostic-ai.org/docs/cli-reference/upgrades/), and [memory](https://agnostic-ai.org/docs/cli-reference/memory/). ## handoff list List saved [task handoffs](https://agnostic-ai.org/docs/handoff/) across the repository's worktrees: ```bash agnostic-ai handoff list ``` Each branch gets one row: live worktree (or `removed`), saved HEAD and date, working changes (`clean`, `dirty`, or `unknown`), task status, PR (`-` when missing), and handoff file. The newest manual note wins over automatic snapshots. It reads only note headers, from the stores the [handoff skill](https://agnostic-ai.org/docs/handoff/#limits-and-overrides) uses. It suggests removal only when local Git ancestry proves the branch merged into the main checkout's branch and no working changes are pending, so squash merges need your review. It never fetches or deletes files. ## revert Undo a `sync --backup`: for every generated file and tool instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `CONVENTIONS.md`, `.agnostic-ai/AGNOSTIC_AI.md`), restore `.bak` with its permissions, never wider than the file's, and remove it. It also restores a nested `CLAUDE.md` that sync deleted. An unfiltered `revert` restores intact recorded backups of removed outputs, including those from dropped targets. Paths under [`sync.unmanaged`](https://agnostic-ai.org/docs/configuration/#syncunmanaged) are never touched. | Flag | Description | |------|-------------| | `-t`, `--only`, `--except` | Select targets, as in [`sync`](https://agnostic-ai.org/docs/cli-reference/sync/#sync). | | `--dry-run` | Show what it would do and change nothing. | | `--force` | Also delete generated files that have no `.bak`, including generated tool instruction files and your own files at the same paths. | | `--json` | Same format as `sync --json`. Actions: `"restore"`, `"remove"`, `"preserve"` (no `.bak`, no `--force`), `"skip"` (already gone). | ## cleanup Remove the `.bak` backups that `sync --backup` wrote, and no other `.bak` files. ```bash agnostic-ai cleanup agnostic-ai cleanup --dry-run # preview deletions ``` ## packs Add, update, and remove collections of shared specs. See the [packs](https://agnostic-ai.org/docs/packs/) guide. ```bash agnostic-ai packs add github.com/obra/superpowers@v6.4.2 agnostic-ai packs add ./path/to/pack agnostic-ai packs list agnostic-ai packs update [name] # at the commit in agnostic.packs.lock agnostic-ai packs update --latest [name] # move to the newest commit of the ref agnostic-ai packs remove superpowers ``` ## hook paths Run inside an edit hook to print the files the edit leaves on disk, one per line. [Edited paths](https://agnostic-ai.org/docs/spec-format/hook-reference/#edited-paths) explains the input, tool detection, and exit codes. ```bash files=$(agnostic-ai hook paths) || exit 1 printf '%s\n' "$files" | grep '\.go$' | while IFS= read -r f; do gofmt -w "$f"; done ``` Capture the output first, as above: piped straight into the loop, a failure is lost and the hook exits 0. | Flag | Description | |------|-------------| | `-t`, `--target ` | Tool that sent the event. Defaults to `AGNOSTIC_AI_TARGET`. | | `--action` | Print each change as `` (`add`, `update`, `delete`, `move`), including deleted files and move sources. | | `--json` | Print each change as a JSON array of `{action, path, from}`. Not with `--action`. | ## hook worktree-remove Remove one clean Git worktree from a Claude Code [`WorktreeRemove`](https://code.claude.com/docs/en/hooks#worktreeremove) event on stdin. Pair it with your `WorktreeCreate` hook; it does not change cleanup for worktrees the host creates without one. ```bash agnostic-ai hook worktree-remove --repo "/path/to/repository" --allowed-root "/path/to/disposable worktrees" ``` | Flag | Description | |------|-------------| | `--repo ` | Required. A surviving checkout of the repository, outside the disposable worktrees. | | `--allowed-root ` | Required. Only descendants of this directory may be removed. | After checking Git's worktree registration and repository metadata, it runs `git worktree remove` without force, keeping the branch. It refuses the main checkout, the allowed root itself, other repositories, unregistered directories, traversal, symlinks below the allowed root, locked worktrees, and worktrees with changed, untracked, or ignored files (ignored dependencies included; remove them first). It also refuses indexes with `assume-unchanged` or `skip-worktree` entries, sparse checkouts included, because those can hide changed contents. | Result | Output | Exit | |--------|--------|------| | Removed | `removed: ` | 0 | | Already gone below the allowed root (also on a repeat call); nothing deleted or pruned | `already absent: ` | 0 | | Unsafe input, event data over 1 MiB, or a failed Git operation | error | 1 | Claude Code runs this event instead of its own cleanup, while the directory still exists. A non-zero exit keeps the directory, with no Git fallback. ## hook guard Run inside an `after-edit` or `stop` hook, as the [spec guard hooks](https://agnostic-ai.org/docs/spec-format/hook-reference/#spec-guard) do. It exits 2 with a short report for the agent on a problem, else 0 with no output. ```bash command -v agnostic-ai >/dev/null 2>&1 || exit 0; agnostic-ai hook guard after-edit command -v agnostic-ai >/dev/null 2>&1 || exit 0; agnostic-ai hook guard stop ``` - `after-edit` reads the event data on stdin, like [`hook paths`](#hook-paths), and reports lint errors in the specs the edit touched. - `stop` reports specs changed without a sync. It passes when the agent already continued from a stop hook, so the notice cannot loop. - It uses the nearest `agnostic-ai.yaml` at or above the current directory, and exits 0 when it cannot read the event data or config. | Flag | Description | |------|-------------| | `-t`, `--target ` | Tool that sent an `after-edit` event. Defaults to `AGNOSTIC_AI_TARGET`. | ## hook memory Print the `MEMORY.md` fact lists, personal first, for a `session-start` hook to add to the model's context. The [`memory` built-in](https://agnostic-ai.org/docs/memory/#load-at-session-start) sets this up, and also runs it on Claude Code when a subagent starts. ```bash agnostic-ai hook memory --target codex ``` | Tool | Output | |------|--------| | Codex, Factory | Plain text | | Cursor | `{"additional_context": ...}` | | Gemini CLI, Qoder | `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": ...}}` | | Copilot | That object for VS Code, plus a top-level `additionalContext` for Copilot CLI | | Claude Code | `{"hookSpecificOutput": {"hookEventName": "SubagentStart", "additionalContext": ...}}` with personal memory only, since `CLAUDE.md` gives subagents the project list. Nothing when `CLAUDE.md` imports the personal list too. | - Output stays under 6,000 bytes, cut at a line end with a note naming the full index. Hidden characters are dropped first (see [session start](https://agnostic-ai.org/docs/memory/#load-at-session-start)), except a zero-width joiner or non-joiner between two non-ASCII characters, as in an emoji or a Persian word. - With no project, it prints nothing. With `memory.personal: repo`, it names the personal folder even before an index exists; in checkout mode, no index means no output. It finds the project as the `shared-memory` skill does. | Flag | Description | |------|-------------| | `-t`, `--target ` | Tool that runs the hook. Defaults to `AGNOSTIC_AI_TARGET`. | ## hook worktree-refresh Rerun Claude Code's configured environment setup in one registered linked worktree: ```bash agnostic-ai hook worktree-refresh --repo /path/to/main --worktree "/path/to/linked worktree" ``` `--repo` is a checkout of the owning repository and `--worktree` the linked checkout. The helper checks that the worktree is registered to that repository and refuses the main checkout. Run `agnostic-ai sync` in that worktree first when setup changes: it resets the completion marker only when the generated setup script matches current source, including output-path overrides such as `outputs.claude.dir`. See [refresh an existing worktree](https://agnostic-ai.org/docs/spec-format/environments/#refresh-an-existing-worktree) for how markers work. - Setup runs from the worktree with a 600-second limit; output and failures pass through. Cancellation stops the setup processes it owns; a successful setup can leave background services running. Other worktrees keep their markers. - On Windows it needs Git Bash, from `CLAUDE_CODE_GIT_BASH_PATH` or the Git for Windows install, never a `bash` launcher from PATH. - It installs dependencies only if your setup command does. It does not check application health or change other tools' setup, and normal Claude setup hooks still run once per worktree. ## hook run Test a synced hook spec without starting a session. See [test a hook](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#hook-run) for the sample events, results, and exit codes. ```bash agnostic-ai hook run protect-files --edit .github/workflows/tests.yml --expect block ``` | Flag | Description | |------|-------------| | `-t`, `--target ` | Test only these tools. Defaults to every configured tool the hook applies to. | | `--edit ` | Send a `PreToolUse` or `PostToolUse` event that edits this file. | | `--bash ` | Send a `PreToolUse` or `PostToolUse` event that runs this shell command. | | `--prompt ` | Prompt text for `UserPromptSubmit`. | | `--runtime vscode` | With `--target cline`, model Cline VS Code TaskStart on Unix instead of the SDK. Windows launcher selection is not modeled. | | `--payload ` | Send this JSON file to every tool, for events the flags above do not cover. | | `--expect allow\|block` | Fail unless every tool gives this result. | | `--include-assumed` | Count [assumed results](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#assumed-results) toward `--expect` and the comparison between tools. | | `--format text\|json` | `json` prints one report with a `targets` array; see the [JSON fields](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#json-output). Defaults to `text`. | ## install-hook Install git hooks; see the [git hooks](https://agnostic-ai.org/docs/git-hooks/) guide. By default it installs a pre-commit hook that runs `project --check --against index`, so a commit fails if regenerated files are not staged. Project hooks prefer the installed local binary over `PATH`. ```bash agnostic-ai install-hook # writes .git/hooks/pre-commit (local) agnostic-ai install-hook --shared # writes .githooks/ and sets core.hooksPath agnostic-ai install-hook --global # checks global specs before committing agnostic-ai install-hook --post-checkout # writes .git/hooks/post-checkout and post-merge agnostic-ai install-hook --post-checkout --shared # writes both hooks in .githooks/ ``` An existing hook keeps its content, and the checks go at its end. A hook that would stop before them (its first line does not select `sh` or `bash`, or it has an `exec` or an unindented `exit`) is left alone, and the command prints the lines to add by hand. - `--shared` writes `.githooks/` at the root of the main working tree, even from a linked worktree. It stops if `core.hooksPath` already points elsewhere. - `--global` is for the global specs folder, which must be the root of its own git repository. The hook runs `lint --global --strict`, `validate --global`, and `sync --global --check` (skipped in a linked worktree). Not with `--shared` or `--post-checkout`; it stops when `core.hooksPath` points elsewhere or the hook already runs the project `sync --check`. - `--post-checkout` installs `post-checkout` and `post-merge`, which run `agnostic-ai project` from the worktree root after a branch or worktree checkout (not a single file) or a merge, pull included. See [regenerate on checkout](https://agnostic-ai.org/docs/git-hooks/#regenerate-on-checkout). ## completion Generate a shell completion script. ```bash agnostic-ai completion bash > ~/.local/share/bash-completion/completions/agnostic-ai # or /etc/bash_completion.d/ agnostic-ai completion zsh > "${fpath[1]}/_agnostic-ai" agnostic-ai completion fish > ~/.config/fish/completions/agnostic-ai.fish agnostic-ai completion powershell | Out-String | Invoke-Expression ``` Restart your shell or `source` the file. Completing `--target` reads `agnostic-ai.yaml` in the current directory, or offers every target if there is none. ## lsp Start the language server on stdin and stdout. The [VS Code extension](https://github.com/Chemaclass/agnostic-ai/tree/main/editors/vscode) starts it for a configured workspace, using `agnostic-ai.binaryPath`; other editors can launch `agnostic-ai lsp` for Markdown and YAML files. - It lints the project's configured sources, custom and absolute directories included, when you open or save a file. Unsaved edits are not linted. - Config and spec load failures appear on the source file, with the parser location when known, or as a workspace error. - A successful check clears resolved diagnostics from every affected file, deleted sources included. After a failed load, earlier diagnostics stay until you fix the problem and save a project file. # Test hook behavior ## Test a hook {#hook-run} `agnostic-ai hook run ` runs a hook spec the way each tool would, before a session fires it. A hook that blocks on Claude Code and does nothing on Codex shows up here, not in a live session: ```text $ agnostic-ai hook run protect-files --edit .github/workflows/tests.yml claude: block (exit 2, 12ms) event: PreToolUse (Write) command: .claude/hooks/protect-files.sh stderr: Blocked: .github/workflows/tests.yml is protected. codex: allow (exit 0, 9ms) event: PreToolUse (apply_patch) command: export AGNOSTIC_AI_TARGET=codex; .codex/hooks/protect-files.sh hook protect-files: targets decide differently: claude block, codex allow ``` For each configured tool the hook reaches, it runs every command sync wrote for that tool, from the project root, or from the handler's `cwd` on Copilot. ### Build sample event data {#event-data-replies} `--edit ` and `--bash ` build a `PreToolUse` or `PostToolUse` tool call (`BeforeTool` or `AfterTool` on Gemini). `--prompt ` builds `UserPromptSubmit` (`BeforeAgent` on Gemini). `SessionStart` takes its `source` from the matcher, or `startup` when the matcher is empty. `--payload ` sends a JSON file as is, for any event. ### Apply matchers {#matcher-replies} When the matcher does not match the tool or source, that tool reports `allow` and why, since it would not run the hook. A `--payload` tool call matches its `tool_name` by the tool's own rules, including Codex's `Edit` and `Write` aliases for `apply_patch`. Cursor, Goose, Copilot, and Kiro match the field their docs name for the event, such as the command, file path, or prompt text. ### Skip unsupported events {#missing-event-replies} A tool without the hook's event, such as Codex for `Notification`, is not run. ### Use the configured timeout {#timeout-replies} The command uses the timeout sync writes, or the tool's default in the table below. ### Report background hooks {#async-replies} An `async: true` hook, or `asyncRewake: true` on Claude Code and Qoder, runs and prints its output, but the tool does not wait for it. Its result is `not judged` and stays out of `--expect` and the comparison. The same holds for Cursor `sessionStart` and `sessionEnd`, Copilot `notification`, and Cline hooks on any event but `PreToolUse` and `PostToolUse`. ### Stop leftover background commands {#background-commands-replies} On macOS and Linux, a command the hook leaves running is killed once the hook exits. ### Find stale generated hook files {#stale-tool-file-replies} Commands come from the spec, so a command that calls a hook script needs a `sync` first to copy the script. A `warning` names the synced file when it is missing, when no handler under the event runs the spec's command, or when that handler's group matcher, timeout, or (on Gemini) `env` differs from the spec. It also compares `failClosed` on Claude Code and Cursor, `cwd` on Copilot, and `confirm` on Kiro. An `env` warning names keys, never values. A warning does not fail the run; `sync` clears it. The file is the one each [tool page](https://agnostic-ai.org/docs/targets/) lists, or the path `outputs` sets. ### Environment variables per tool `hook run` clears these variables from the calling shell's env, then sets them per tool. Other variables pass through. | Target | Variables set | |--------|---------------| | Claude Code | `AGNOSTIC_AI_TARGET=claude`, `CLAUDE_PROJECT_DIR` | | Codex | none; its command sets the target itself | | Gemini | `GEMINI_PROJECT_DIR`, `GEMINI_CWD`, `GEMINI_SESSION_ID`, `CLAUDE_PROJECT_DIR`, then the handler's `env`, which holds `AGNOSTIC_AI_TARGET=gemini` | | Trae | `TRAE_PROJECT_DIR`, `CLAUDE_PROJECT_DIR` | | OpenHands | `OPENHANDS_PROJECT_DIR`, `OPENHANDS_SESSION_ID`, `OPENHANDS_EVENT_TYPE`, `OPENHANDS_TOOL_NAME` | | Goose | `PLUGIN_ROOT`, with `AGNOSTIC_AI_TARGET=goose` from its command | | Augment | `AUGMENT_PROJECT_DIR`, `AUGMENT_CONVERSATION_ID`, `AUGMENT_HOOK_EVENT`, `AUGMENT_TOOL_NAME` | | Cursor | `CURSOR_PROJECT_DIR`, `CURSOR_VERSION` (empty), `CLAUDE_PROJECT_DIR`, and `AGNOSTIC_AI_TARGET=cursor`, which the `sessionStart` entry that sync adds sets for later hooks | | Crush | `CRUSH=1`, `AGENT=crush`, `AI_AGENT=crush`, `CRUSH_EVENT`, `CRUSH_TOOL_NAME`, `CRUSH_SESSION_ID`, `CRUSH_CWD`, `CRUSH_PROJECT_DIR`, `CRUSH_TOOL_INPUT_COMMAND` or `CRUSH_TOOL_INPUT_FILE_PATH` when the tool input has one, and editor and pager variables that keep anything from waiting for a terminal; `AGNOSTIC_AI_TARGET=crush` comes from its command | | Copilot | the handler's `env`, which holds `AGNOSTIC_AI_TARGET=copilot`. Copilot is not run when a value holds `$`. | | Factory | `FACTORY_PROJECT_DIR` | | Qoder | `QODER_PROJECT_DIR`, then the handler's `env`, which holds `AGNOSTIC_AI_TARGET=qoder` | | Antigravity | none | | Cline | none; the script exports `AGNOSTIC_AI_TARGET=cline` | | Kiro | `USER_PROMPT` on `UserPromptSubmit` | | Windsurf | `DEVIN_PROJECT_DIR` | ### Shell and default timeout per tool "Script path only" and "assumed" cells are [assumed results](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#assumed-results). | Target | Shell | Default timeout | |--------|-------|-----------------| | Claude Code | `bash -c`, on Windows with Git Bash (`CLAUDE_CODE_GIT_BASH_PATH`, else the Git install that holds `git.exe`), or PowerShell when there is none and the spec sets no `shell`; exec-form `args` with no shell; `shell: powershell` with PowerShell | 600 seconds, except 30 on `UserPromptSubmit`, `PreModelSwitch`, and `PostModelSwitch`, and 10 on `MessageDisplay` | | Codex | `sh -c`; on Windows, `commandWindows` with `powershell.exe -Command` | 600 seconds | | Gemini | `bash -c`, or Windows PowerShell on Windows, after Gemini replaces `$GEMINI_PROJECT_DIR`, `$GEMINI_CWD`, `$GEMINI_SESSION_ID`, and `$CLAUDE_PROJECT_DIR` with the quoted root | 60 seconds | | Trae | `bash -c`, or PowerShell on Windows | 30 seconds | | OpenHands | `/bin/sh -c`, or `cmd.exe /c` on Windows | 60 seconds | | Goose | `sh -c` on every platform | 30 seconds | | Augment | A `.sh`, `.ps1`, `.cmd`, or `.bat` script path only, run directly on macOS and Linux, and on Windows with `powershell.exe -Command` (`.ps1`) or `cmd.exe /c` (`.cmd`, `.bat`). An inline command is not run. | 60 seconds | | Cursor | Script path only | Assumed 30 seconds | | Crush | Crush's embedded POSIX shell, on every platform. A path starting with `./`, `../`, or `/` runs through its shebang interpreter, or with no shebang as shell source. A path without that prefix, such as `hooks/guard.sh`, starts as a program; on macOS and Linux one without a shebang then fails with exit 1. | 30 seconds | | Copilot | Exec-form `args` with no shell. A `command` is script path only. | 30 seconds | | Factory | Script path only, where `"$FACTORY_PROJECT_DIR"` counts as a plain word | 60 seconds | | Qoder | Exec-form `args` with no shell; `shell: bash` with `bash -c` on macOS and Linux. Without `shell`, script path only, where `"$QODER_PROJECT_DIR"` or `"${QODER_PROJECT_DIR}"` counts as a plain word. `shell: powershell`, exec form with a `$` in `command` or `args`, and a shell-form command on Windows are not run. | 600 seconds | | Antigravity | Script path only | 30 seconds | | Cline | `bash`, running the event script sync writes. Not run on Windows. | 120 seconds on `PreToolUse` and `PostToolUse`; sync drops `timeout` | | Kiro | Script path only | 60 seconds, and assumed 60 for `timeout: 0` | | Windsurf | Script path only | Assumed 30 seconds | ### Claude Code `if` rules A handler's `if` permission rule decides whether it runs, as in Claude Code. It applies only on `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, and `PermissionDenied`. See the [`if` field](https://code.claude.com/docs/en/hooks) and [permission rule syntax](https://code.claude.com/docs/en/permissions#permission-rule-syntax). - `Bash(git *)` matches any part of a command split by `&&`, `||`, `;`, `|`, and `&`, and commands inside `$()` and backticks, past leading `VAR=value` assignments and wrappers such as `timeout 30`. A trailing ` *` also matches the bare command, and `:*` equals ` *`. - A pattern that names more than the command runs on any `$()`, backtick, or `$VAR`. A command that does not parse always runs. - `Edit(path)` covers `Edit`, `Write`, `MultiEdit`, and `NotebookEdit`, with gitignore patterns: `//path` from the filesystem root, `~/path` from home, `/path` and `path` from the project root, and a bare name such as `.env` at any depth. `Tool(param:value)` matches a top-level input field. ### Decisions {#decisions} Each command reports one decision. - `block`: exit 2, or exit 0 with a JSON reply that has `"permissionDecision": "deny"`, `"decision": "block"`, or `"continue": false`. - `allow`: any other exit 0. - `error`: any other non-zero exit. - `timeout`: the command ran past its timeout. With `failClosed: true`, Claude Code treats a hook failure as exit 2 for its event, and on `PermissionRequest` denies the request. [Claude Code hook mappings](https://agnostic-ai.org/docs/targets/claude-reference/#hook-mappings) lists the events and handlers that ignore it. Exit 2 cannot stop anything on `SessionStart`, `SessionEnd`, `Notification`, `PreCompact`, and `PostCompact`, so it reads as `error` there. On `PostToolUse` the tool already ran, so `block` sends stderr back to the model. A `context` line marks output the tool adds to the session: plain stdout on `SessionStart` and `UserPromptSubmit`, or a JSON reply's `additionalContext`. ### How other tools read replies ### Gemini replies Reads stdout, or stderr when stdout is empty. A JSON `"decision": "deny"` or `"block"`, `"continue": false`, or plain text with an exit other than 0 and 1 is `block`. No text, or exit 1, is `error`. `SessionStart`, `SessionEnd`, `Notification`, and `PreCompress` ignore decisions, so a block there reads as `error`. Only a JSON `additionalContext` reaches the model; plain stdout goes to the user. ### Trae replies As Claude Code. ### OpenHands replies Blocks only on `PreToolUse`, `UserPromptSubmit`, and `Stop`: on exit 2, or a JSON `"decision": "deny"` or `"continue": false` at any exit code. A `context` line marks a JSON `additionalContext` on `UserPromptSubmit` and `Stop` only. ### Goose replies Blocks only on `PreToolUse` and `Stop`: on exit 2, or stdout starting with `{` whose `decision` is `"block"`, at any exit code. Exit 0 with empty stdout or `"decision": "allow"` allows. Anything else is `error`, or `block` with `x-goose.on_failure: block`. ### Cursor replies Exit 2 blocks on `beforeSubmitPrompt` and the permission events: `beforeShellExecution`, `beforeMCPExecution`, `beforeReadFile`, `beforeTabFileRead`, `subagentStart`, and `preToolUse`. At exit 0, a permission event blocks on `"permission": "deny"`, on `"ask"` (except `preToolUse`, which does not enforce it), and on output that is not a valid JSON reply; no output fails open. `beforeSubmitPrompt` blocks on `"continue": false`. Other failures fail open as `error`, or block with `failClosed: true`. ### Crush replies Exit 2 blocks the tool call; exit 49 also halts the whole turn, with a note. Other non-zero exits and a timeout do not block. At exit 0, a JSON `"decision": "deny"` or `"halt": true` blocks. A reply with a `hookSpecificOutput` key reads as Claude Code's, and its top-level fields are ignored. A field of the wrong type voids the reply, which then allows. A `context` line marks `context` or `additionalContext`. ### Factory replies Exit 2 blocks, except on `Notification`, `SubagentStop`, `PreCompact`, `SessionStart`, and `SessionEnd`, where it reads as `error`. At exit 0, `PreToolUse` blocks on `permissionDecision` `"deny"` or `"ask"`; `PostToolUse`, `UserPromptSubmit`, `Stop`, and `SubagentStop` on `"decision": "block"`; and every event but `SessionEnd` on `"continue": false`. ### Copilot replies Drops `{"type": "progress"}` lines from stdout and parses the rest as one JSON reply; text that does not parse, or two objects, is no reply. `preToolUse` blocks on any non-zero exit, a command that did not start, or `"permissionDecision"` `"deny"` or `"ask"`. A failure other than exit 2 also counts as `error`, so `--expect block` fails on a hook that never ran. `permissionRequest` blocks on exit 2 or `"behavior": "deny"`; replies merge in order, so a later `"behavior": "allow"` overrides an earlier deny. `agentStop` and `subagentStop` block on `"decision": "block"`. Other events cannot block: failures read as `error`, except exit 2 on `postToolUseFailure`, which adds context. A timeout fails open everywhere. PascalCase events decide as their camelCase twins. ### Qoder replies Exit 2 blocks on `UserPromptSubmit`, `PreToolUse`, `Stop`, `SubagentStop`, `PreCompact`, `ConfigChange`, `Elicitation`, and `ElicitationResult`, and any non-zero exit blocks on `WorktreeCreate`. Other failures read as `error`; `StopFailure` and `InstructionsLoaded` ignore the result. At exit 0, a JSON reply blocks on: - `"continue": false` - `"decision": "deny"`, where exit 2 blocks - `permissionDecision` `"deny"` or `"ask"` on `PreToolUse`, which wins over `decision` - a `decision.behavior` of `"deny"` on `PermissionRequest` - an `action` of `"decline"` or `"cancel"` on the elicitation events A `hookSpecificOutput` without `hookEventName` voids the reply as `error`. A `ConfigChange` with `source: policy_settings` cannot block, so a block there reads as `error`. `TaskCreated`, `TaskCompleted`, `TeammateIdle`, and `Setup` have no documented decision rules, so Qoder is not run for them. A handler's `if` glob in parentheses matches the `command` of `Bash` or the `file_path` of a file tool; a glob on another tool is not run. ### Antigravity replies Only exit 0 with a JSON reply of the documented shape counts; anything else reads as `error` or `timeout`, not counted. On `PreToolUse`, `"decision": "auto_approve"` is `allow`, with a note that explicit user rules still apply; `"deny"` blocks; `"force_ask"` blocks, with a note that Antigravity asks the user. `"allow"` (which defers to the next hook or default policy) reads as `allow`, and `"ask"` and `"deny_unless_prior_grant"` (which saved permissions can let through) read as `block`; `hook run` cannot see either, so none of the three is counted. On `Stop`, `"decision": "continue"` keeps the agent running and reads as `block`; any other value allows. `PostToolUse`, `SessionStart`, `PreInvocation`, and `PostInvocation` allow on a JSON object. `hook run` builds no `SessionStart` payload; pass `--payload `. ### Cline replies Ignores the exit code and reads stdout as JSON: the last line starting `HOOK_CONTROL` and a tab, else the whole stdout. On `PreToolUse` and `PostToolUse`, `"cancel": true` blocks and stops the Cline run; on `PostToolUse` the tool already ran. Empty stdout and other JSON allow, and a non-zero exit adds a note that Cline ignores it. Stdout that is not JSON reads as `error` and a timeout as `timeout`, each with a note that Cline goes on as if the hook allowed. A `context` line marks `context`, `contextModification`, or `errorMessage`. When other specs reach Cline on the same event, sync writes them into one script that shares stdout, so the result is not counted, even with `--include-assumed`. ### Kiro replies CLI V3 blocks on exit 2 on `PreToolUse`. Another non-zero exit there reads as `error`, not counted, since Kiro's docs disagree on whether it blocks. `UserPromptSubmit` allows every exit code, with a note that only the IDE blocks prompts; a non-zero exit adds the output and exit code to context. `Stop` reads exit 1, or exit 0 with `"decision": "block"`, as `block`, with a note that Kiro keeps the agent running. Other non-zero exits on `PostToolUse` and `Stop` read as `error`. A `context` line marks prompt stdout, non-zero prompt exits, and a Stop continuation's `reason`. ### Windsurf replies Devin CLI blocks on exit 2, or exit 0 with `"decision": "block"`, on `PreToolUse`, `PermissionRequest`, `UserPromptSubmit`, and `Stop`. `"decision": "approve"`, no decision, and plain stdout allow. Another non-zero exit is `error`. Not counted: a block on `PostToolUse`, `PostCompaction`, `PreCompact`, `PostCompact`, `SessionStart`, or `SessionEnd`; a reply on a non-zero exit, except exit 2 with `"decision": "block"`; and a `decision` other than `"approve"` or `"block"`. A `PreToolUse` reply with `hookSpecificOutput.updatedInput` gets a note, since Devin CLI merges it into the tool's arguments. A `context` line marks `hookSpecificOutput.additionalContext` on `UserPromptSubmit`, `SessionStart`, and `PostToolUse`. Both count only when `hookSpecificOutput.hookEventName` names the event that ran. ### Augment replies Exit 2 blocks on `PreToolUse` only. Exit 0 blocks with `permissionDecision: "deny"`, `decision: "block"` (inside `hookSpecificOutput` on `Stop` and `PostToolUse`), or `"continue": false`. A `context` line marks plain stdout on `SessionStart` and `hookSpecificOutput.additionalContext` at exit 0, as on Claude Code. The run exits 1, so it works as a CI check, when a counted command times out or errors, when two counted tools decide differently, or, with `--expect allow` or `--expect block`, when a counted tool decides otherwise. ### Assumed results {#assumed-results} Some tools' docs leave out part of how a command runs. `hook run` runs those commands on an assumption instead of skipping them, ends the result line in `(assumed: shell, timeout)`, and adds one line per assumption. An assumed result stays out of `--expect` and the comparison unless you pass `--include-assumed`. A warning flags an assumed result that disagrees with the counted ones, and a summary such as `0 checked, 1 assumed (not counted; --include-assumed to count)` shows what was left out. With `--expect`, a run where only assumed results ran fails and asks for `--include-assumed`, so a CI check never passes on nothing. In JSON, each target has `assumptions` (`item`, `value`, `reason`) and `counted`. | Assumption | Targets | What `hook run` assumes | |---|---|---| | Shell | Cursor, Factory, Antigravity, Kiro, Windsurf, Copilot's `command` form, Qoder without `shell` | `sh -c` on macOS and Linux, only for a script path with plain or single-quoted arguments. A command with shell syntax, such as a pipe, is not run, with a note to use a script path; Windows is not run. Copilot's and Qoder's exec form (`args`) runs with no shell and assumes none. | | Timeout | Cursor, Windsurf | 30 seconds when the spec sets none. Set `timeout` to remove it. | | Timeout | Kiro | 60 seconds for `timeout: 0`, which Kiro documents as no limit. | | Working directory | Copilot | The project root for a hook without `cwd`. With `cwd`, a relative exec path resolves from it. | | Working directory | Factory, Qoder, Antigravity, Windsurf | The project root, since the docs do not say. A command starting `"$FACTORY_PROJECT_DIR"/` or `"${QODER_PROJECT_DIR}"/` counts as a script path, with the variable set to the root; Windsurf sets `DEVIN_PROJECT_DIR` the same way. | | Working directory | Cline | The project root, where Cline uses the directory the CLI started in. | | `jq`, `coreutils` | Crush | Crush has built-in `jq` and, on Windows, `cat`, `ls`, `rm`, `find`, and the other core utilities, unless `CRUSH_CORE_UTILS=false`. `hook run` runs the ones on `PATH` and adds an assumption for each it reached, only for inline commands and `./` scripts without a shebang. | ### Counted and assumed results A `not counted` result is one the replies above mark `not counted`. It stays out of `--expect` and the comparison even with `--include-assumed`, though with that flag a timeout or error from another of that tool's commands still fails the run. Kiro is not run for an `agent` action, which runs no command; a hook with `confirm`, which asks the user first; and every trigger but `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop`, whose event data the docs do not show. A Cursor hook spec uses Cursor's own event names, so other tools are not run. A Copilot hook spec with a PascalCase event such as `PreToolUse` also runs on Claude Code, which counts, so `--expect` checks Claude Code and shows Copilot beside it. The Cline VS Code extension does not read stdout when the script exits non-zero, so a reply printed before a failing exit blocks in the CLI and not in the extension. ## JSON output `--format json` prints the same results as one JSON object. Each target has a `decision` (`allow`, `block`, `error`, `timeout`, or `not run` with a `reason`), its `warnings`, its `assumptions`, whether it is `counted`, and one entry per command. `exit_code` is `null` after a timeout or a command that did not start. `error` holds the reason the run fails. The exit code is the same as in text: ```json { "hook": "protect-files", "targets": [ { "target": "claude", "decision": "block", "event": "PreToolUse", "trigger": "Write", "async": false, "commands": [ { "command": ".claude/hooks/protect-files.sh", "decision": "block", "exit_code": 2, "elapsed_ms": 12, "timed_out": false, "stdout": "", "stderr": "Blocked: .github/workflows/tests.yml is protected.\n", "adds_context": false } ], "notes": [], "warnings": [], "assumptions": [], "counted": true } ] } ``` ## Sample event inputs A tool that lacks the input a flag needs is not run, and the other tools still run. In each row, `tool_response` holds placeholder values, and session IDs and transcript paths are made up. Where a section lists several tool names, `hook run` calls the first one the matcher matches. Gemini, Trae, Goose, Cursor, Crush, Windsurf, and Augment read the matcher as an unanchored regular expression. ### Claude Code sample events `--edit` calls `Write`, `Edit`, or `MultiEdit`, with an absolute `tool_input.file_path`. `SubagentStart` needs no input and starts `general-purpose`, `Explore`, or `Plan`. ### Codex sample events `--edit` calls `apply_patch` with an `*** Add File:` or `*** Update File:` patch in `tool_input.command`, and `Edit` and `Write` match it. ### Gemini sample events `--edit` calls `write_file` or `replace`, with an absolute `tool_input.file_path`; `--bash` calls `run_shell_command`. A `SessionStart` source must match exactly. ### Trae sample events `--bash` calls `RunCommand`. Trae lists no edit tool input, so Trae is not run on `--edit`. ### OpenHands sample events `--bash` calls `terminal`. The file editor's input is undocumented, so OpenHands is not run on `--edit`. A `*` or empty matcher matches all; one with a regex character, or written `/re/`, must match the whole tool name; any other is an exact name. ### Goose sample events `--bash` calls `shell`; `--edit` calls `write` or `edit`. `BeforeShellExecution` and `AfterShellExecution` take `--bash`, and `AfterFileEdit` and `BeforeReadFile` take `--edit`. The matcher applies to `matcher_context`; one that does not compile, such as `*`, skips the rule. ### Cursor sample events `--bash` builds `beforeShellExecution` and `afterShellExecution`, and `preToolUse` and `postToolUse` on the `Shell` tool. `--edit` builds `afterFileEdit` only, so on `preToolUse` and `postToolUse` Cursor is not run. `--prompt` builds `beforeSubmitPrompt`. ### Crush sample events `hook run` builds only `PreToolUse` and reports other Crush events as not run, so `--prompt` does not apply. `--bash` calls `bash`; `--edit` calls `edit` or `write`. The matcher applies to the lowercase tool name; one that does not compile makes Crush skip the hook, and `hook run` reports it. ### Factory sample events `--bash` calls `Execute`. `--edit` calls `Create`; when the matcher picks `Edit` or `ApplyPatch`, Factory is not run. `--prompt` builds `UserPromptSubmit`. Empty or `*` matches everything, letters and `\|` list exact names, and anything else is a case-sensitive regular expression. ### Antigravity sample events `--bash` calls `run_command`; `--edit` calls `write_to_file`, `replace_file_content`, or `multi_replace_file_content`. Antigravity has no prompt event; `PreInvocation`, `PostInvocation`, and `Stop` take `--payload`. The matcher must match the whole tool name; empty or `*` matches everything. ### Copilot sample events `--bash` builds `preToolUse` and `postToolUse` on `bash`, and `PreToolUse` on `Bash`. `--prompt` builds `userPromptSubmitted` or `UserPromptSubmit`. `--edit` is refused, and so is `--bash` on `PostToolUse`. A camelCase matcher is anchored as `^(?:PATTERN)$`. Copilot runs JavaScript regexes, so a matcher with inline flags, lookaround, backreferences, `(?P)`, POSIX classes, or Go-only escapes such as `\A` is not run. ### Qoder sample events `--bash` calls `Bash`. `--edit` calls `Write`; when the matcher picks `Edit`, Qoder is not run. Empty or `*` matches everything, letters and `\|` list exact names, and anything else is a regular expression. ### Cline sample events `--bash` calls `run_commands`; `--edit` calls `editor` (a model whose id holds `gpt` or `codex` calls `apply_patch` instead, which a note says). `--prompt` builds `UserPromptSubmit`, with a note that the CLI may not send that event. Cline has no matcher, so every hook fires; a portable `match` kind runs the script's tool name check, which allows a call to another tool. `PreCompact` is not run. ### Kiro sample events `--prompt` supplies `UserPromptSubmit`; `Stop` needs no input. Tool events require `--payload`; `--bash` and `--edit` are refused. CLI V3 ignores prompt and Stop matchers. See [Kiro matchers](https://agnostic-ai.org/docs/cli-reference/hook-diagnostics/#kiro-hook-run-matchers). ### Windsurf sample events `--bash` builds `PreToolUse`, `PostToolUse`, and `PermissionRequest` on `exec`. `--prompt` builds `UserPromptSubmit`; `Stop` and `PostCompaction` need no input. `--edit` is refused. `SessionStart`, `SessionEnd`, `PreCompact`, and `PostCompact` take `--payload`. The matcher applies to `tool_name`, so a Claude-style `Bash` matcher does not fire. `PreCompact` and `PostCompact` accept `manual` or `auto`; a matcher on another event is not run. ### Augment sample events `--bash` calls `launch-process`; `--edit` calls `str-replace-editor` or `save-file`. Augment has no prompt event. ## Matcher and plugin limits Gemini matchers compile as Go regular expressions, which reject a few JavaScript forms such as lookahead. Gemini CLI would run those, and `hook run` compares them as a literal name. `GEMINI_PLANS_DIR` is not set. OpenCode and Kilo run hooks as plugins and Zed as tasks, so they are not run. OpenCode V2 plugins receive native event JSON on stdin; the command does not simulate their runtime. ### Kiro matchers {#kiro-hook-run-matchers} Kiro tool matchers without regular-expression syntax select tool IDs, the documented `read`, `write`, `shell`, and `web` categories, or source tags. They support `*` and `?` wildcards and also try an unanchored regular expression against the tool ID. A matcher with regular-expression syntax uses that expression only. `@mcp`, `@builtin`, `@server`, and `@server/tool` select MCP or built-in tools. `execute_bash` and `execute_pwsh` selectors match each other. CLI 2.x aliases are not expanded. `@powers`, `spec`, `subagent`, and `context` are not run because their complete tool membership is undocumented. A named MCP selector is also not run when underscores make its server/tool boundary ambiguous. Use an anchored regular expression against the internal tool ID. ## Sources Vendor docs and pinned source each tool's hook behavior is checked against: - **Claude Code:** [Hooks guide](https://code.claude.com/docs/en/hooks-guide). - **Codex:** [Hooks](https://learn.chatgpt.com/docs/hooks). - **Copilot:** [Hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference), [using hooks with Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks). - **Cursor:** [Hooks](https://cursor.com/docs/hooks). - **Gemini CLI:** [Hooks reference](https://geminicli.com/docs/hooks/reference/), [file system tools](https://geminicli.com/docs/tools/file-system/), source [`c6bccb7`](https://github.com/google-gemini/gemini-cli/tree/c6bccb7ecbf6d8368d995455dd725ed34466faad/packages/core/src/hooks). - **Qoder:** [Hooks](https://docs.qoder.com/cli/hooks), [hooks reference](https://docs.qoder.com/cli/hooks-reference). - **Factory:** [Hooks](https://docs.factory.com/harness/hooks). - **Devin CLI:** [Hooks](https://docs.devin.ai/cli/extensibility/hooks), [commands triggered by session events](https://docs.devin.ai/cli/extensibility/hooks/lifecycle-hooks). - **Antigravity:** [Hooks](https://antigravity.google/docs/hooks). - **Kiro:** [Hooks](https://kiro.dev/docs/hooks), [hook types](https://kiro.dev/docs/hooks/types), [hook actions](https://kiro.dev/docs/hooks/actions), [CLI V3 migration](https://kiro.dev/docs/cli/v3/hooks-migration), [built-in tools](https://kiro.dev/docs/tools). - **Trae:** [automate actions with hooks](https://docs.trae.cn/ide_automate-actions-with-hooks), [hook reference](https://docs.trae.cn/ide_hook-configuration-reference). - **Augment:** [Hooks](https://docs.augmentcode.com/cli/hooks). - **OpenHands:** [Hooks](https://docs.openhands.dev/openhands/usage/customization/hooks), source [`fad6377`](https://github.com/OpenHands/software-agent-sdk/tree/fad63774459171b08f889b12fd3b4d6346168c3e/openhands-sdk/openhands/sdk/hooks). - **Goose:** [Hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks/), source [`bab8ff6`](https://github.com/aaif-goose/goose/blob/bab8ff641039c9cd3331121cd84a5c6045f365ca/documentation/docs/guides/context-engineering/hooks.md) and [runner](https://github.com/aaif-goose/goose/blob/bab8ff641039c9cd3331121cd84a5c6045f365ca/crates/goose/src/hooks/mod.rs). - **Cline:** source [`39ff235`](https://github.com/cline/cline/tree/39ff2359f7e08231281539696e48a166ce49270c/sdk/packages/core/src/hooks): [`hook-file-hooks.ts`](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/sdk/packages/core/src/hooks/hook-file-hooks.ts#L234-L254), [`subprocess-runner.ts`](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/sdk/packages/core/src/hooks/subprocess-runner.ts#L68-L97), [tool schemas](https://github.com/cline/cline/blob/39ff2359f7e08231281539696e48a166ce49270c/sdk/packages/core/src/extensions/tools/schemas.ts#L149-L224). - **Crush:** source [`76cc5c5`](https://github.com/charmbracelet/crush/tree/76cc5c574e15072b15aaed0f4f843a5711fae0d9/internal): [`config/config.go`](https://github.com/charmbracelet/crush/tree/76cc5c574e15072b15aaed0f4f843a5711fae0d9/internal/config/config.go), [`hooks/input.go`](https://github.com/charmbracelet/crush/tree/76cc5c574e15072b15aaed0f4f843a5711fae0d9/internal/hooks/input.go), [`hooks/runner.go`](https://github.com/charmbracelet/crush/tree/76cc5c574e15072b15aaed0f4f843a5711fae0d9/internal/hooks/runner.go), [`shell/dispatch.go`](https://github.com/charmbracelet/crush/tree/76cc5c574e15072b15aaed0f4f843a5711fae0d9/internal/shell/dispatch.go), [`shell/run.go`](https://github.com/charmbracelet/crush/tree/76cc5c574e15072b15aaed0f4f843a5711fae0d9/internal/shell/run.go). - **`hook run` shell:** [mvdan.cc/sh](https://github.com/mvdan/sh) and its [core utilities](https://github.com/mvdan/sh/blob/b5028a3332a4d5d5a6cc62a999c1ba7993eb3627/x/coreutils/coreutils.go), [gojq](https://github.com/itchyny/gojq). # Project CLI and readiness ## project Run the project's installed agnostic-ai binary after checking its version against the project's `requires` and any exact stable version in `package.json`. It prefers `node_modules/.bin/agnostic-ai` over `PATH`; when the project declares an agnostic-ai dependency, a global binary never stands in for the local one. On Windows it runs the package's Node launcher directly. ```bash agnostic-ai project # sync --keep-edits --quiet agnostic-ai project --check # sync --check, no install or writes agnostic-ai project --check --against index agnostic-ai project --bootstrap # repair dependencies once, then sync agnostic-ai project -- hook memory --target codex ``` - It finds the nearest project config at or above the working directory; a built-in hook can use the host's project-directory variable instead. - A mismatch names the installed and required versions and the `agnostic-ai project --bootstrap` recovery. Conflicting exact declarations stop before any install. npm ranges, tags, aliases, prereleases, and file dependencies are not read as requirements; state those in `requires`. - With `--against index` or `HEAD`, the version contract comes from the staged files or commit, and the binary from the working checkout. - Version and command checks share ten seconds. A failed check shows up to 64 KiB of the command's error output, marking cut-off text, timeouts, and cancellation. Warnings from a successful command do not fail it. - A binary without `project` (such as v0.82.0) gets an instruction to upgrade the owning package through its package manager. | Flag | Description | |------|-------------| | `--check` | Check the version and generated output without installing or writing. | | `--against index\|HEAD` | With `--check`, use the existing check for staged or committed output. | | `--bootstrap` | If the binary is missing or mismatched, install once from the npm, pnpm, or bun lockfile, then sync preserving manual edits. | Bootstrap picks the package manager from `packageManager` or `devEngines.packageManager` in `package.json`, or an unambiguous root lockfile, and runs `npm ci`, `pnpm install --frozen-lockfile`, or `bun install --frozen-lockfile`. It needs a declared agnostic-ai dependency and a committed lockfile (`bun.lock` or `bun.lockb` for bun) that installs an allowed version; it never changes `requires` or the manifest. Other package managers get an error naming their install command. Package scripts run as usual. `AGNOSTIC_AI_PROJECT_BOOTSTRAP=1` stops a nested helper call from installing again; postinstall can still regenerate output. With the right binary in place, it installs nothing. A CLI that supports `project` must already be installed, globally or locally; hook managers can then call it without changing their own config. ### Project readiness Before starting a cached environment, check the installed project binary and generated files from the project root, in a shell that stops on failure: ```bash set -e agnostic-ai project --check agnostic-ai project -- doctor --scope project --check-references ``` Start your environment after these lines, so a failed check stops it. The second command checks every configured target, including local Markdown links in generated skills, without approving hooks on this machine. The project binary must support `doctor --scope project` (v0.82.0 does not). Keep the package pin, lockfile, and `requires` aligned. - Missing or mismatched project binary: run `agnostic-ai project --bootstrap`, then repeat the checks. - Only ignored generated files missing: run `agnostic-ai project`. - Drift: move manual edits into the source first. A checkout hook that warns and lets Git continue does not prove startup is ready. These checks cover project configuration, not running services. # Upgrade and migrate ## upgrade Update agnostic-ai using the method you installed it with. `update` is an alias. ```bash agnostic-ai upgrade agnostic-ai upgrade --check # show install details without updating agnostic-ai upgrade --version v0.81.0 # install a specific release ``` `upgrade` updates the installed tool. If the latest release is already installed but the project still requires an older one, the message points to `agnostic-ai upgrade --requires`. That command updates the project requirement and schema URL, then runs sync. ### Automatic upgrade offers In a terminal, a stable release build checks for updates at most once a day, with a five-second network limit, and offers: ```text Update available: 0.83.0 -> 0.85.0 Release guidance: https://github.com/Chemaclass/agnostic-ai/releases/tag/v0.85.0 Update now? [Y/n] ``` Enter or `y` accepts. `n` runs your original command and silences the offer for the day. In a project, accepting: 1. Installs the new release and checks the new executable's version. 2. Raises `requires` to that release, keeping its form, and updates the editor schema link, as [`--requires`](#upgrade-options) does. 3. Previews and applies the [migrations](#migrate) to old spec files. 4. Runs `sync`, then `sync --check`. Outside a project, it updates only the tool. Then rerun your command; the release page lists manual steps. - A failed install or version check starts no project change. A later failed step stops, keeps what finished, and prints how to continue. - If a config file, `.agnostic-ai`, or `.agnostic-ai/local` links to another project or the global specs folder, the offer is skipped and names it. Links within the project are fine. - Project-local npm or pnpm installs, and npx, get a command to run yourself. Automatic npm updates apply only to confirmed global installs (a two-second check). - `AGNOSTIC_AI_NO_UPDATE_CHECK=1` turns the checks off. They are silent offline and skip CI, redirected input or output, JSON output, quiet mode, global commands, checks, previews, hooks, completion, editor services, and development builds. ### Upgrade options | Flag | Description | |------|-------------| | `--check` | Show install details without changing anything. With `--version`, also show `Requested:`. | | `--version ` | Install a specific release, including an older one. The leading `v` is optional. For package-manager installs, choose the version through that manager instead. | | `--requires` | Raise this project's `requires` to the installed release, keeping its form, update the editor schema link, then sync. See below. | | `--run` | Accepted for compatibility; upgrading is already the default. | After updating through a package manager, run its installed CLI from the project root, as in `pnpm exec agnostic-ai upgrade --requires`. `--requires` changes the base config and any existing local `requires`, keeping comments and other settings. With 0.91.0 installed: | Current value | New value | |---------------|-----------| | missing, empty, or `null` | `">=0.91.0"` | | `">=0.90.0"` | `">=0.91.0"` | | `"0.90.0"` | `"0.91.0"` | | `"=0.90.0"` | `"=0.91.0"` | | a range or upper bound, such as `">=0.90.0 <0.91.0"` or `"<0.91.0"` | `">=0.91.0"` | A missing key is added only to the base config. When a range becomes a minimum, the command prints the value it replaced. If sync fails, the version changes stay: fix the problem and run `sync` again. It needs a stable release build, also reads `agnostic.config.yaml`, and does not combine with `--check`, `--version`, or `--run`. The global config and package-manager files are left to their own commands. The config needs one root setting per line and a single-line `requires`; rewrite a root `{...}`, YAML `<<` merges, or a `requires` shared through `&`/`*` or split across lines first. | Binary location | Upgrade | |-----------------|---------| | `*/Cellar/*`, `*/Caskroom/*`, `/opt/homebrew/*`, `/home/linuxbrew/.linuxbrew/*` | `brew update && brew upgrade --cask Chemaclass/tap/agnostic-ai` | | `$GOBIN` or `$GOPATH/bin` (defaults to `$HOME/go/bin`) | `go install github.com/chemaclass/agnostic-ai/cmd/agnostic-ai@latest` | | `*\scoop\apps\*`, `*\scoop\shims\*` | `scoop update agnostic-ai` | | `*\Microsoft\WinGet\*` | `winget upgrade Chemaclass.agnostic-ai` | | `*/node_modules/*` | `npm install -g agnostic-ai@latest` | | Standalone binary on macOS or Linux | `upgrade` downloads the release, checks its checksum and its [build provenance](https://agnostic-ai.org/docs/verify-a-release/#build-provenance), and replaces the binary. | | Standalone binary on Windows | Use the [PowerShell install script](https://agnostic-ai.org/docs/installation/). Windows cannot replace a running program. | The Scoop, WinGet, and `node_modules` paths match in any letter case. `upgrade` also reports other copies on `PATH` that would run instead of the one it found. ## migrate Update spec files that use old field names or formats. Old forms keep working, and sync writes the same files afterward, except that a literal MCP credential becomes a `${NAME}` reference. `doctor` and `upgrade --requires` name the migrations that apply. ```bash agnostic-ai migrate --list # which migrations apply here agnostic-ai migrate --dry-run # preview the rewrites agnostic-ai migrate # apply them agnostic-ai migrate --global # rewrite the global specs ``` | Flag | Description | |------|-------------| | `--dry-run` | Print each rename and a diff of each rewrite, and write nothing. Every `env` and `headers` value, `args` item, URL, and credential-like value prints as ``; a `${NAME}` reference and a `!literal` tag still show. See [credential previews](https://agnostic-ai.org/docs/cli-reference/sync/#credential-previews). | | `--list` | List every migration with its release and whether it applies here. It names each pack whose author needs to update its specs. | | `--only ` | Run only these groups, comma-separated. A migration ID starts with its group, such as `config-file-name` in group `config`. | | `--global` | Rewrite the specs in `$AGNOSTIC_AI_HOME` (default `~/.agnostic-ai`) and its `local/` folder. | - A migration rewrites only what keeps its meaning; anything else stays, with the reason. `secrets-mcp-literals` names each variable to set, never the value. - A symlinked spec keeps its link, and its target gets the rewrite. A pack, or a spec linked into one, is never rewritten; the output names the pack. - Migrations stay within the project (or, with `--global`, the global specs folder) and its local folder, even a linked one. - A migration that cannot plan, such as on a spec that does not parse, prints `cannot plan` with the reason; the others run, and `migrate` exits 1. - `doctor` names a migration only when it rewrites something or a skip needs you, such as a spec setting both old and new forms. `--dry-run` and `--list` show every skip. | Migration | Release | Rewrites | |-----------|---------|----------| | `config-file-name` | 0.79.0 | `agnostic.config.yaml` to `agnostic-ai.yaml`, in a project only. When both exist with the same content, it removes the old file; it skips when they differ or Git ignores `agnostic-ai.yaml`. | | `hooks-portable-events` | 0.79.0 | A hook's `event` and `matcher` to the [portable](https://agnostic-ai.org/docs/spec-format/hook-reference/#portable-events) `on` and `match`, such as `PreToolUse` on `Bash` to `before-tool` on `shell`. Comments, quoting, and key order stay. Skipped when the portable form would change a tool's event or matcher (such as `matcher: Read` on Codex, or `matcher: Edit\|Write` on Claude Code, since `match: edit` also covers `MultiEdit` and `NotebookEdit`), when a `local/` spec extends the hook, and for a pack's hook. | | `capabilities-agent-tools`, `capabilities-settings-permissions`, `capabilities-skill-tools` | 0.79.0 | Agent `tools` to `can`, and Claude Code names in permissions and skill `allowed-tools` to names shared across tools. An adjacent `WebFetch, WebSearch` pair becomes `web`; names without a shared form stay. Run with `--only capabilities`. | | `secrets-mcp-literals` | 0.79.0 | Plain MCP `env` and `headers` values: credentials become `${NAME}` references, named as `import` names them, and other values get [`!literal`](https://agnostic-ai.org/docs/spec-format/mcps/#plain-settings). Skips keys with credential names whose values do not look like credentials, credentials mixed with a reference, and pack specs. Run with `--only secrets`. | # Memory commands ## memory Check, list, locate, and repair the [shared memory](https://agnostic-ai.org/docs/memory/) in the working directory: project memory in `.agnostic-ai/memory/`, then personal memory in `.agnostic-ai/local/memory/` (or the [shared personal folder](https://agnostic-ai.org/docs/memory/#one-store-per-repository) with `memory.personal: repo`). Missing folders are skipped. ```bash agnostic-ai memory lint # run only the memory checks agnostic-ai memory index # rebuild each MEMORY.md from its fact files agnostic-ai memory list # print each fact's scope, type, and title agnostic-ai memory path # print each memory folder's absolute path agnostic-ai memory import --from claude # copy facts from another memory store ``` - `memory lint` reports [LINT039 to LINT044](https://agnostic-ai.org/docs/cli-reference/check/#lint), the memory findings `lint` and `doctor` also report. It exits 1 on an error, or on a warning with `--strict`, and exits 0 when no memory folder exists. `--json` prints the `lint --json` format with `command` set to `memory lint`. - `memory index` drops merge conflict markers, links to missing files, and repeated links, keeping every other line. It then adds a `- [name](file.md): description` line, from the fact's YAML fields, for each fact without one. - `memory list` prints facts in index order, titled by the index line's link text, or the fact's `name` when no line links it. The type comes from `metadata.type`, or a top-level `type` as Claude Code's auto memory writes it. - `memory path` prints one `scope folder` line per memory folder, project first, as absolute paths, even for a folder not created yet. It works from any folder in the project and from a linked worktree; with `memory.personal: repo`, every worktree gets the same personal folder. Tools without a session-start hook run it, as the [`shared-memory-policy` rule](https://agnostic-ai.org/docs/memory/#how-each-tool-loads-it) asks. - `memory import` copies fact files into personal memory, which every tool reads, and adds their index lines. `--from claude` reads Claude Code's store from before sync pointed it at personal memory; `--from checkout` and `--from repo` read the other personal folder after a `memory.personal` switch; `--path` reads any folder with a `MEMORY.md` and one file per fact. The source stays unchanged, and so does an existing destination file. Source index lines that link no fact become a fact of their own (`claude-index-notes.md` for `--from claude`). Symlinks and files with a line that looks like a secret are skipped and named. An identical fact without an index line gets its line back, so a rerun finishes an interrupted import. Import refuses a personal folder holding tracked files, and first adds the checkout folder to Git's exclude list if Git would track it. It records handled facts in `.imported` there, so LINT043 stops counting facts you later delete or merge. | Flag | Description | |------|-------------| | `--strict` | With `memory lint`, exit 1 on warnings too. | | `--json` | With `memory lint`, print `{version, command, findings}`. | | `--from` | With `memory import`, the store to copy from: `claude`, `checkout`, or `repo`. | | `--path` | With `memory import`, a folder to copy from instead of `--from`. | | `--only` | With `memory import`, copy only this fact file; repeat the flag for more. | | `--dry-run` | With `memory import`, list each fact and its description and write nothing. | --- # CI CI (continuous integration) checks on each change that the generated files match your specs. Pick the recipe for how your team stores them, committed in Git or ignored and generated during setup, and run it from the project root after [installing the CLI](#install-the-cli-in-ci). ## Committed outputs Fail when the committed tool files no longer match the specs: ```bash agnostic-ai sync --check ``` `--check` compares the files sync would write with the files on disk, writes nothing, and exits non-zero on a missing or changed file. To fix it, run `agnostic-ai sync` locally and commit the result. Never run `sync` right before this check in CI, because it hides the problem you are testing for. On a Windows runner, Git writes CRLF line endings on checkout, and the check reports those files as `line endings only`. Add a `.gitattributes` rule such as `* text eol=lf`, or use `--against HEAD`, which reads the line endings Git stores. ### Check committed files after another step rewrites the working tree If an earlier step already rewrote the files, as a `postinstall` sync does, check against the commit instead: ```bash agnostic-ai sync --check --against HEAD ``` It builds the files from the committed specs and compares them with the committed files. A committed file that no spec produces anymore also fails, which catches leftovers without `doctor`. See [what `--against` compares](https://agnostic-ai.org/docs/cli-reference/sync/#against-details). `actions/checkout` fetches one commit by default. Set `fetch-depth: 2` so the check has the parent commit (in a pull request's merge commit, the base branch). Without it, a note says the comparison was skipped. ## Ignored outputs A fresh checkout has no generated files. Check the specs and confirm that sync works: ```bash agnostic-ai validate agnostic-ai sync ``` Add `agnostic-ai lint` to check spec quality. Do not add `sync --check` after `sync`: the files were just written. In a Node workspace that pins the CLI and syncs on `postinstall`, `pnpm install --frozen-lockfile` already runs `sync` (see [Node monorepos](https://agnostic-ai.org/docs/git-hooks/#node-monorepos)). For a cached environment that must stop before startup when project configuration is missing or stale, use the [project readiness checks](https://agnostic-ai.org/docs/cli-reference/project/#project-readiness). They check every configured tool without approving local hooks. ### Install a pinned CLI in your CI job ## Install the CLI in CI ### Node projects Pin the npm package as a dev dependency and sync on `postinstall`, as [Node monorepos](https://agnostic-ai.org/docs/git-hooks/#node-monorepos) shows. With ignored generated files, `npm ci` already runs `postinstall`, so the job only checks the specs: ```yaml steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npx agnostic-ai lint --strict && npx agnostic-ai validate ``` With committed generated files, skip `postinstall` so it cannot rewrite the files before the check: ```yaml steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci --ignore-scripts - run: npx agnostic-ai sync --check --diff ``` ### Other projects `scripts/install.sh` installs the release binary on Linux and macOS. Pin the version so local and CI behavior match: ```yaml steps: - uses: actions/checkout@v4 - name: Install agnostic-ai run: | export AGNOSTIC_AI_INSTALL_DIR="$HOME/.local/bin" curl -fsSL https://raw.githubusercontent.com/Chemaclass/agnostic-ai/main/scripts/install.sh | bash echo "$AGNOSTIC_AI_INSTALL_DIR" >> "$GITHUB_PATH" env: AGNOSTIC_AI_VERSION: vX.Y.Z - run: agnostic-ai sync --check --diff ``` `AGNOSTIC_AI_VERSION` takes a release tag; without it, the script installs the latest release. The script checks the archive against the release checksum, and checks [where the archive was built](https://agnostic-ai.org/docs/verify-a-release/#build-provenance) when the GitHub CLI is logged in. GitHub-hosted runners include the GitHub CLI; set `{% raw %}GH_TOKEN: ${{ github.token }}{% endraw %}` to log it in, and `AGNOSTIC_AI_VERIFY_ATTESTATION: 1` to fail when the check cannot run. On Windows runners, use `install.ps1`: see [Installation](https://agnostic-ai.org/docs/installation/#pin-a-version-or-directory). ## Diagnose drift Drift means generated files differ from what your current specs produce. Run `agnostic-ai sync --check --diff` to see the changes. The [CLI reference](https://agnostic-ai.org/docs/cli-reference/sync/#reading-a-failing---check) explains the output and the failure types. ### Check the coding tool against tasks from your project ## Gate model and CLI changes `sync --check` proves the generated files match your specs. It does not show whether a model or CLI still gives good results for your project. To check that, configure a script that runs the AI tool against tasks from your project: ```yaml verify: command: [./scripts/verify-harness] ``` Add the gate after installing the AI CLI it needs: ```yaml - name: Verify Codex project behavior run: agnostic-ai verify --target codex ``` `verify` first checks that generated files are current, then sends JSON to the script on stdin. Your script runs the tasks and decides whether the results pass. Its stdout, stderr, and non-zero exit code reach CI unchanged. See the [`verify` command](https://agnostic-ai.org/docs/cli-reference/check/#verify) for the JSON format. --- # Spec packs A pack is a set of source specs (agents, skills, rules, hooks, and MCP server connections) shared as a Git repository or local folder. Use one to reuse conventions across projects instead of copying spec files. ## Install Review the pack's instructions and scripts before using them. ```bash agnostic-ai packs add github.com/obra/superpowers@v6.4.2 agnostic-ai packs add ./shared/security-rules ``` `packs add` copies the pack into `.agnostic-ai/packs//` and records its version in `agnostic.packs.lock`. Then run `agnostic-ai sync` to generate its files for your tools. A project spec with the same name wins; see [which spec wins](#which-spec-wins). ### Find example packs ## Packs you can install Checked on 2026-10-03. Each repo keeps its skills in a root `skills/` directory. - `github.com/anthropics/skills`: Anthropic's example skills. Installs as `skills`. - `github.com/obra/superpowers@v6.4.2`: a development workflow skill set. Installs as `superpowers`. - `github.com/vercel-labs/agent-skills`: Vercel's web and deployment skills. Installs as `agent-skills`. ## List, update, remove ```bash agnostic-ai packs list agnostic-ai packs update # all packs, at the recorded commits agnostic-ai packs update superpowers # one pack agnostic-ai packs update --latest # move to the newest commit of each ref agnostic-ai packs remove superpowers ``` `update` installs each Git pack at the commit `sha` in the lockfile, so a moved tag or a force-pushed branch does not change what you get. It fails and leaves the lockfile unchanged when the remote no longer has that commit. `update --latest` fetches the newest commit of each pack's `ref`, prints the old and new commit, and records the new one. A local folder pack is copied again as it is now. The lockfile comes from the repository, so `update` checks every entry before it changes any file. It stops on a pack name that starts with a dot or is not a single folder name, and on a Git source other than `https://`, `ssh://`, `git@host:path`, or a host path such as `github.com/foo/bar`. It skips a pack from a local folder outside the project and prints the `packs add` command that refreshes it; `packs update ` stops with that command instead. `packs add` refuses the same names and Git sources. ### Share a pack, choose its source, and inspect its lockfile ## Sources | Source | Example | |------------------|--------------------------------------| | Git URL | `github.com/foo/bar`, `gitlab.com/x` | | Git URL with a tag or branch | `github.com/foo/bar@v1.2.0` | | Local directory | `./local/pack`, `file:///abs/path` | ## Pack layout A pack has the standard source layout at its root: ``` / ├── agents/ ├── skills/ ├── rules/ ├── hooks/ └── mcps/ ``` Leave out any directory you do not need. Spec front matter follows the [spec format](https://agnostic-ai.org/docs/spec-format/). ## Lockfile `agnostic.packs.lock` sits at the project root: ```yaml version: 1 packs: - name: superpowers source: github.com/obra/superpowers ref: v6.4.2 sha: 8ca22dba9a94f28898bbce59f2537ff4d87c747d ``` Commit it so teammates and CI install the same commits. ## Which spec wins When two specs share a kind and name, the later one in this list wins: 1. Built-in specs (when enabled with `builtins`) 2. Pack specs 3. Your project specs 4. Your personal setup, which changes the winning spec field by field instead of replacing it (see [local overrides](https://agnostic-ai.org/docs/local-overrides/)) To adapt a pack convention to one project, add a project spec with the same name. A project rule named `conventional-commits` replaces the pack rule with that name. --- # Use Composer with Claude Code and Codex [Composer](https://getcomposer.dev/) runs coding sessions through Claude Code or Codex. Select both as targets in agnostic-ai so both providers share one set of project instructions, rules, and skills. Composer passes the project directory to the provider, which loads its native files. This guide was checked against the [Composer CLI 0.1.12 source](https://github.com/atemaguer/composer/tree/cli-v0.1.12). Release downloads were unavailable, so packaged runtime behavior and signed-in sessions were not tested. ## Prepare the project [Install agnostic-ai](https://agnostic-ai.org/docs/installation/). Install Composer through its [official project](https://github.com/atemaguer/composer#readme), then install and sign in to the providers you use: [Claude Code](https://agnostic-ai.org/docs/targets/claude/#first-rule) and [Codex](https://agnostic-ai.org/docs/targets/codex/#first-rule). Work from your project root. Create `agnostic-ai.yaml`, or add both targets to your existing list: ```yaml targets: [claude, codex] ``` ### Keep existing provider files Before the first sync, import any existing Claude Code or Codex configuration: ```bash agnostic-ai import claude codex --dry-run --diff agnostic-ai import claude codex ``` Compare the imported `.agnostic-ai/` sources with the original files and resolve differences before syncing. Skip this step in a fresh project. See [how imports from several tools merge instructions](https://agnostic-ai.org/docs/migration/#2-review). ## Write the shared sources ```bash mkdir -p .agnostic-ai/rules .agnostic-ai/skills ``` Add a project note to `.agnostic-ai/AGNOSTIC_AI.md`, keeping imported text: ```markdown When summarizing a change, name the files you inspected. ``` Save this rule as `.agnostic-ai/rules/commit-subjects.md`: ```markdown --- name: commit-subjects description: Use the team's commit subject prefixes. alwaysApply: true --- Start suggested commit subjects with fix:, feat:, docs:, or chore:. ``` Save this skill as `.agnostic-ai/skills/change-summary.md`: ```markdown --- name: change-summary description: Summarize a supplied change. Use when asked for a change summary. --- 1. State the effect a user sees. 2. Suggest a commit subject. 3. List the checks performed, or say that no checks were run. ``` For more fields, use the [rules](https://agnostic-ai.org/docs/spec-format/rules/) and [skills](https://agnostic-ai.org/docs/spec-format/skills/) references. ## Generate and inspect The first sync can replace imported files. `--backup` keeps the originals as `.bak` files. ```bash agnostic-ai lint agnostic-ai sync --plan --backup ``` Review the plan, then generate and check: ```bash agnostic-ai sync --backup agnostic-ai sync --check ``` | Provider | Files to inspect | |---|---| | Claude Code | `CLAUDE.md`, `.claude/rules/commit-subjects.md`, `.claude/skills/change-summary/SKILL.md` | | Codex | `AGENTS.md`, `.agents/skills/change-summary/SKILL.md` | The note appears in both `CLAUDE.md` and `AGENTS.md`. Codex gets the rule inside `AGENTS.md`. For later changes, edit `.agnostic-ai/` and sync again. Home instructions and skills use [global configuration](https://agnostic-ai.org/docs/global-configuration/). Additional settings, hooks, agents, and MCP servers keep their [Claude Code](https://agnostic-ai.org/docs/targets/claude-reference/) or [Codex](https://agnostic-ai.org/docs/targets/codex-reference/) mappings. ## Check each provider in Composer From the project root, start one session per provider: ```bash composer run --provider claude --permission-mode default 'Use the change-summary skill for this supplied change: a broken documentation link was fixed. Read the project instructions first. Do not edit files.' composer run --provider codex --permission-mode default 'Use the change-summary skill for this supplied change: a broken documentation link was fixed. Read the project instructions first. Do not edit files.' ``` Each answer should have a `fix:` or `docs:` subject, the skill's three steps, and inspected filenames that match the generated files above. `sync --check` proves the files match their sources; this session check proves the installed provider loads and follows them. Composer's [run command](https://github.com/atemaguer/composer/blob/c4f04d94839a666f21548616b220fa8eb17a503f/apps/cli/src/index.ts#L552) defaults to Full access in 0.1.12, and its permission mode can override provider configuration, so keep `--permission-mode default`. A noninteractive run stops when approval is required; to answer approvals, use `composer tui` and review the provider and permission mode there. Composer's [skill catalog](https://github.com/atemaguer/composer/blob/c4f04d94839a666f21548616b220fa8eb17a503f/packages/composer-runtime/src/capabilities.ts#L24) scans global provider skill folders and the Codex plugin cache, not project skill folders, and [selecting a skill in the TUI](https://github.com/atemaguer/composer/blob/c4f04d94839a666f21548616b220fa8eb17a503f/apps/cli/src/tui/components/dialogs/DialogActions.tsx#L87) shows a notice instead of running it. Ask the provider to use the project skill, as above. --- # Local overrides Put personal specs and instructions in `.agnostic-ai/local/` to layer them on top of the team's shared setup. Git ignores the folder, so your preferences and experiments stay on your machine. For example, create `.agnostic-ai/local/AGNOSTIC_AI.md`: ```markdown Answer in Spanish. ``` Then run `agnostic-ai sync`. The text extends the shared project instructions. Generated instruction files are ignored by default; if your project commits them, your personal text appears there too, so review before committing. For personal configuration values, use `agnostic-ai.local.yaml` (see [configuration](https://agnostic-ai.org/docs/configuration/#local-overrides)). In `agnostic-ai ui`, **Personal (not committed)** on **Settings** saves built-ins and settings fields as local overrides; see [saving for yourself](https://agnostic-ai.org/docs/cli-reference/inspect/#edit-scope). ## Add a spec A local spec with a new name adds a spec for your machine. A local spec with the same kind and name as a shared one [changes only the fields you write](#override-fields). Run `agnostic-ai sync`, then `agnostic-ai list` to check which source won. ### Choose project-local or machine-wide personal sources | Name | Folder | Loaded by | Inspect with | |---|---|---|---| | `project-user` | `/.agnostic-ai/local/` | `agnostic-ai sync` in that project | `agnostic-ai list` | | `global-local` | `~/.agnostic-ai/local/` (or `$AGNOSTIC_AI_HOME/local/`) | `agnostic-ai sync --global` | `agnostic-ai list --global` | A folder loads only when it exists. Project sync ignores `~/.agnostic-ai/`, and global sync ignores every project. Your personal setup wins over the shared one. ## Layout `local/` mirrors the source directory: ```text my-project/.agnostic-ai/ ├── AGNOSTIC_AI.md # shared, committed ├── rules/testing.md ├── skills/review/SKILL.md └── local/ # yours: ignored, still synced ├── AGNOSTIC_AI.md # extends the shared instructions ├── rules/scratch-notes.md # new name: appends └── skills/review/SKILL.md # same name: edits the shared skill ``` The project folder reads every spec kind: `agents/`, `skills/`, `rules/`, `hooks/`, `mcps/`, `commands/`, `settings/`, `reviews/`, `environments/`, and `ignore/`. It stays at `.agnostic-ai/local/` even when custom `sources` paths in `agnostic-ai.yaml` move the shared specs. For the global folder’s supported kinds and layout, see [global configuration](https://agnostic-ai.org/docs/global-configuration/#source-and-targets). ### Change shared fields, extend bodies, and keep supporting files ## Override fields A local spec with the same kind and name as a shared one merges into it. Write only what changes; when the team edits the shared spec, the next sync keeps your changes on top. Shared, committed: ```md --- name: custom description: Draft the release notes. model: claude: sonnet codex: gpt-5.6-terra --- Read the merged PRs since the last tag. ``` Local, ignored: ```md --- name: custom model: claude: opus --- ``` After sync, Claude uses `opus`; the description, body, and Codex model stay as shared. Frontmatter (the YAML fields between `---` lines) merges key by key, the same way `agnostic-ai.local.yaml` merges over `agnostic-ai.yaml`: | Local value | Result | |---|---| | a group of keys | merges into the shared group, key by key, at any depth | | a single value or a list | replaces the shared value | | `null` | removes the key | | missing | keeps the shared value | So `tools: [Read]` replaces the whole shared list, and `tools: null` drops it. In a settings spec, a local per-tool `model` or `effort` map merges over a shared single value as if it were the map's `default`: shared `model: sonnet` with local `model: {codex: gpt-5.5}` gives Codex `gpt-5.5` and every other tool `sonnet`. A local `default` still wins. Inside an `x-` map, `null` removes one key: `x-codex.model: null` drops `model` for Codex only. The local file's location sets its scope. `local/rules/style.md` applies to the whole project even when the shared `rules/backend/style.md` applies to `backend/` only. YAML specs (hooks, MCP servers, settings, environments) follow the same rules: a local `mcps/docs.yaml` can change `env.PORT` and keep the shared command and args. ## Turn a shared spec off List the targets you do not want in `targets-exclude`: ```md --- name: review targets-exclude: [claude, codex, cursor] --- ``` ## Extend the body - **Empty local body.** The shared body stays. - **A `::parent` line.** The shared body goes there. Put your text before it, after it, or around it. - **Any other body.** It replaces the shared one. Shared: ```md --- name: custom --- Review the changed code. ``` Local: ```md --- name: custom --- ::parent Then run my local benchmark. ``` The result is the shared review instruction followed by your benchmark step. `::parent` must start at column 0 and stand alone on its line. Keep it outside a `::target` fence. In a spec with a new name, `::parent` has nothing to extend, so sync drops it. ## Skill assets A local skill folder with only a `SKILL.md` keeps the shared folder's supporting files, such as scripts and templates. If the local folder holds any other file, its own supporting files replace the shared supporting files. ## What does not merge Only personal overrides merge field by field. A project spec replaces a [pack](https://agnostic-ai.org/docs/packs/) spec with the same kind and name in full. Two files with the same name in one folder are a mistake: one wins, and `agnostic-ai lint` reports the other (LINT003). ## Extend the instructions `.agnostic-ai/local/AGNOSTIC_AI.md` extends `.agnostic-ai/AGNOSTIC_AI.md`. Sync appends its text to every root instruction file it writes (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, and the rest), after the shared body and any inlined rules: ```md Answer in Spanish. Run `make test` before every commit. ``` - A legacy [`outputs..rules-file`](https://agnostic-ai.org/docs/configuration/#entry-point-files) that names the entry point gets the block too, after the rule bodies. - `::target` fences and `@path` imports work as in the [shared file](https://agnostic-ai.org/docs/configuration/#per-target-paragraphs). In the global folder, `local/AGNOSTIC_AI.md` comes last in the managed instructions block, after the shared instructions and rules. ## Import `agnostic-ai import` never writes your local specs into `.agnostic-ai/`, even though sync puts them in the native files import reads (such as a local rule inlined into `AGENTS.md`). Import skips every name your local folder declares and lists what it skipped. A shared spec your local folder extends keeps its own content. Hooks are matched by event, matcher, and handler. A local handler is removed from the imported hook, and a script that only local hooks run stays out of `.agnostic-ai/scripts/`. Settings, reviews, and environments share one native file per target. If your local folder holds one of these kinds, import leaves every shared spec of that kind unchanged and says so. ## Stay out of Git `init` and `sync` add `/.agnostic-ai/local/` to the managed `.gitignore` block, next to `/agnostic-ai.local.yaml`. Add nothing by hand. See [gitignore](https://agnostic-ai.org/docs/configuration/#gitignore). If `~/.agnostic-ai/` is a Git repository, add this line to its `.gitignore`: ```gitignore /local/ ``` ## See what wins `agnostic-ai list` prints each spec and the source that won: ```console $ agnostic-ai list skill review project-user rule testing project rule scratch-notes project-user ``` `agnostic-ai list --global` does the same for the `global` and `global-local` setups. ## Watch `sync --watch` re-syncs when you edit a local spec or `.agnostic-ai/local/AGNOSTIC_AI.md`. See [`sync --watch`](https://agnostic-ai.org/docs/cli-reference/sync/#sync). --- # Directory-specific instructions Use a scoped rule for instructions that apply to one directory, such as a service's conventions. Write the rule once and sync writes each supported tool's files, which keeps that text out of the root instructions every session loads. ## Start with one directory Run commands from the project root. For a new scratch project: ```bash echo "claude,codex" | agnostic-ai init agnostic-ai new rule payments-context --scope services/payments ``` In an existing project, skip `init` and check [tool compatibility](#shared-files-and-safe-updates). If `new rule --help` does not list `--scope`, [upgrade agnostic-ai](https://agnostic-ai.org/docs/installation/#upgrade). Edit `.agnostic-ai/rules/payments-context.md`: ```markdown --- name: payments-context scope: services/payments --- Use integer minor units for monetary values. ``` Generate and check: ```bash agnostic-ai sync agnostic-ai sync --check agnostic-ai graph --spec payments-context ``` Claude gets a rule for that directory. Codex gets `services/payments/AGENTS.md`; start Codex inside `services/payments` so it loads the nested file. See [native support](#native-support) for other tools. To change the rule, edit the source and sync again. Preview with `agnostic-ai render .agnostic-ai/rules/payments-context.md`, or trace a generated file with `agnostic-ai why services/payments/AGENTS.md`. ## Check what applies to a file ```bash agnostic-ai explain --file services/payments/handler.go --target cursor ``` The report lists each instruction for one tool (Claude Code, Codex, or Cursor), with its source spec, output path, condition, and status. It reads the output sync plans to write, so it shows what is set to apply, not what the model loaded. A shared `services/payments/AGENTS.md` replaces a `.mdc` rule when another tool needs it. See the [CLI reference](https://agnostic-ai.org/docs/cli-reference/inspect/#explain-a-source-file) for the status values. ### Set directory scope and understand Codex directory patterns ## Scope rules - `scope` is a project-relative directory and everything under it, with `/` separators. Absolute paths, `..`, glob characters, symlinks that leave the project, and `scope: .` are rejected. Omit `scope` for project-wide rules. - A folder under `rules/` that names a project directory scopes the rules in it: `.agnostic-ai/rules/services/payments/limits.md` has scope `services/payments`. - A `scope:` in the frontmatter wins over the folder. A folder that names no project directory only groups rules, so `rules/modules/a.md` with `scope: src/a` is scoped to `src/a`. - In installed packs, a rule folder sets a scope only when it names a directory in your project, not one inside the pack. - `lint` warns when a rule's folder names a project directory and its `scope:` points elsewhere (LINT020). - Keep rule names unique across directories. - Deeper rules add local context. The tool decides whether parent instructions load and which wins. - `alwaysApply: true` adds no files to the combined scope and patterns. Sync sets the tool's own condition flags. - Scope does not remove instructions already loaded into a conversation. ## Codex rules with globs alone Globs are filename patterns such as `src/api/**`. With Codex enabled, an unscoped rule with `globs: [src/app/api/**, prisma/**]` writes `src/app/api/AGENTS.md` and `prisma/AGENTS.md` instead of adding to the root instructions. Other tools that read `AGENTS.md` share those files. `alwaysApply: true` keeps an unscoped rule project-wide. Set `outputs.codex.nested-glob-rules: false` to keep all unscoped rules inline. Every pattern must cover a whole directory tree; sync never widens a file filter to a whole directory. Patterns such as `src/api/**/*.ts` or `Dockerfile`, or a mix of root files and directory patterns, keep the whole rule inline in the root file, with a note naming the always-loaded rule. The rule also stays inline, with a note, for output overrides, unmanaged destinations, and tools that read the root file without verified nested-file support, whether configured or chosen with `--target`. `on-unsupported: error` refuses this fallback. An inlined rule with `globs` begins with an `Applies to:` line, such as ``Applies to: `resources/js/**` ``, so it does not read as a rule for the whole repository. Starting Codex at the root does not load nested files for later edits. ## Qwen Code rules with globs alone An unscoped rule with `globs` or `paths` writes `.qwen/rules/.md` with the same patterns as a `paths:` list. Qwen Code leaves it out of the prompt until a tool reads or edits a matching file, and loads project rules only in a trusted folder. `alwaysApply: true` keeps the rule in `QWEN.md`. A legacy `outputs.qwen.rules-file` or a moved `outputs.qwen.file` keeps every rule in that file. See [Qwen Code rules](https://qwenlm.github.io/qwen-code-docs/en/users/features/rules/). ### Combine directory scope with file patterns ## Narrow a rule to certain files `scope` plus `globs` or `paths` applies to the whole scope directory **and** every matching file. Use both for a module and its tests: ```yaml scope: services/payments globs: "tests/payments/**" ``` Claude writes both `services/payments/**` and `tests/payments/**` in `paths`. Codex writes `AGENTS.md` in both directories. Patterns are relative to the project, not the scope, including `**/*.go`. A pattern inside the scope adds no files. To apply a rule only to certain files, omit `scope` and keep the rule outside a folder that implies scope. `paths` and `globs` work the same way. Unsupported combinations warn and skip; set `on-unsupported: error` to fail instead, or `silent` to suppress notices. **Breaking change:** earlier versions applied a rule only where scope and patterns overlapped. Remove `scope` from rules that used it to narrow `globs`, and write the full project-relative filters instead. Remove a catch-all `globs: "**/*"` from a rule that should apply only to its scope. See [migration](https://agnostic-ai.org/docs/migration/#scope-and-pattern-unions). ### Selector limits by target - Tools that read a file per directory accept whole-directory patterns such as `tests/payments/**`. They reject file filters such as `tests/payments/**/*.go` instead of applying them to every file in that directory. Root patterns such as `CHANGELOG.md` and `**/*` are also unsupported with scope on those tools. Tools with file filters keep those patterns. - The tool's own keys `regex`, `applyTo`, `fileMatchPattern`, and `glob` cannot be combined with `scope`. - Cline's empty `paths` array keeps the rule disabled. Continue's empty `globs` array is unsupported with scope. - Windsurf, Trae, and Antigravity write the rule in the project's rules directory when a pattern reaches outside the scope. - Imported file patterns without an explicit `scope` keep their exact behavior; their folder adds no files. A tool with no equivalent skips that rule under `on-unsupported`. - Tools that take a list keep literal commas inside a pattern. Tools that take one comma-separated string reject those commas. Commas inside brace patterns such as `tests/{a,b}/**` work everywhere. ### Check scoped-rule support and runtime limits by tool ## Native support Mappings were checked against vendor documentation on 2026-09-09. Tests verify the generated files, not how each live product behaves. | Target | Scoped file or condition | Vendor reference | |---|---|---| | Claude | `.claude/rules//.md`, `paths` | [Memory](https://code.claude.com/docs/en/memory) | | Mistral Vibe | `/AGENTS.md` | [Project context](https://docs.mistral.ai/vibe/code/cli/configuration-reference) | | Codex | `/AGENTS.md` | [Instructions](https://developers.openai.com/codex/guides/agents-md) | | Gemini | `/GEMINI.md` | [Context](https://geminicli.com/docs/cli/gemini-md/) | | Cursor | `.cursor/rules//.mdc`, conditional `globs`; shared nested `AGENTS.md` when compatible peers use it | [Rules](https://cursor.com/docs/context/rules) | | Copilot | `.github/instructions/.instructions.md`, `applyTo` | [Host support](https://docs.github.com/en/copilot/reference/custom-instructions-support) | | Cline | `.clinerules//.md`, `paths` | [Rules](https://docs.cline.bot/customization/cline-rules) | | Windsurf / Devin | `/.devin/rules/.md`, glob trigger | [Rules](https://docs.devin.ai/cli/extensibility/rules) | | Antigravity | `/.agents/rules/.md`, glob trigger | [Rules](https://antigravity.google/docs/rules) | | Continue | `.continue/rules//.md`, `globs` without `alwaysApply` | [Rules](https://github.com/continuedev/continue/blob/main/docs/customize/deep-dives/rules.mdx) | | Amp | `/AGENTS.md` | [Instructions](https://ampcode.com/docs/customize/agents-md) | | Warp | `/AGENTS.md` | [Rules](https://docs.warp.dev/agents/capabilities/rules/) | | OpenCode | `/AGENTS.md` | [Rules](https://opencode.ai/docs/rules/) | | Kiro | `.kiro/steering/.md`, `fileMatchPattern` | [Steering](https://kiro.dev/docs/steering/) | | Trae | `/.trae/rules/.md`, conditional `globs` | [Rules](https://docs.trae.ai/ide/rules) | | Goose | `/AGENTS.md`, or nested `.goosehints` with its legacy opt-in | [Context files](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/context-engineering/using-goosehints.md) | | Augment | `/AGENTS.md` | [Rules](https://docs.augmentcode.com/cli/rules) | | Qoder | `.qoder/rules//.md`, `paths` | [CLI memory](https://docs.qoder.com/cli/memory) | | OpenHands | `.agents/skills//SKILL.md`, `paths` | [Path rules](https://docs.openhands.dev/overview/skills/path) | | Factory | `/AGENTS.md` | [Instructions](https://docs.factory.com/harness/agents-md) | | Kilo | `/AGENTS.md`, without unconditional `instructions` entries | [Instructions](https://kilo.ai/docs/customize/agents-md) | Aider, Zed, Junie, Crush, and Jules have no verified automatic directory scope, so they skip scoped rules. Root rules still work. Runtime limits: - Codex and OpenCode load instructions from the directory they start in and its parents. Warp loads the root and current directory, and may find files in other directories. Gemini loads context as files are accessed. - Copilot support varies by host. OpenHands path rules work in local conversations, not ACP. - Qoder Desktop and Kiro custom-agent loading are not yet checked at runtime. ## Shared files and safe updates Tools that read the same generated file must agree on its text and loading conditions. Sync refuses incompatible combinations; neither a partial sync (`--only`) nor `prefer-spec` bypasses the check. Use compatible tools or separate worktrees. ### Resolve shared files, output overrides, and old scoped outputs - Kiro loads nested `AGENTS.md` files everywhere, so it conflicts with tools that write them. - Crush reads `.cursor/rules` without applying its conditions, so it conflicts with scoped Cursor rules. - Tools that share a nested `AGENTS.md` need the same scoped rules and the same text. Keep headers enabled. Tools that read a file per directory reject `file` and `rules-dir` overrides. Scoped rules reject `rules-file` overrides, except Goose's `.goosehints` opt-in. For hand-written files or conflicting aliases, follow [migration](https://agnostic-ai.org/docs/migration/#keep-directory-specific-instructions). After you move or delete a scope, run a full sync to remove the old files; a partial sync keeps the files of tools it skips. Backups and revert cover scoped outputs too. See [troubleshooting](https://agnostic-ai.org/docs/troubleshooting/#scoped-rules) for common errors. --- # Shared memory Shared memory lets one coding tool save a project fact that every other tool reads next session. Facts are Markdown files, so your team reviews shared facts in Git. ## Turn it on Add `memory` to `agnostic-ai.yaml` and sync once: ```yaml requires: ">=0.81.0" builtins: [memory] ``` ```bash agnostic-ai sync ``` Saving a fact needs no further sync. To enable it across projects, use [global configuration](https://agnostic-ai.org/docs/global-configuration/). Pick project or global setup, not both, or the same facts can load twice. Claude Code needs a project sync either way. ## Two memories | | Project memory | Personal memory | | --- | --- | --- | | Folder | `.agnostic-ai/memory/` | `.agnostic-ai/local/memory/` | | In Git | Yes, shared with the team | No, stays on your machine | | Holds | Team facts: conventions, decisions, known problems | Your preferences and corrections | | Tools save | After you confirm | Without asking | Each folder has one file per fact and a `MEMORY.md` list. Tools read the list when a session starts and open a fact when they need it. ``` .agnostic-ai/memory/ MEMORY.md # - [CI runs on Ubuntu](ci-ubuntu.md): PR CI skips macOS and Windows ci-ubuntu.md ``` ## Save and recall a fact Ask your coding tool: “Remember that this project's integration tests need the local database running.” It proposes the team fact for your approval. A personal preference, such as shorter answers, it can save without asking. Then start another tool in the project and ask what it knows about integration tests. Keep your coding tool's own memory on: shared memory adds a layer every tool reads. Your request wins over saved facts. See [Gemini CLI](https://agnostic-ai.org/docs/targets/gemini/#shared-memory) for how it keeps its own memory alongside these facts. Ask for the `shared-memory` skill to recall a topic or clean up stale facts. Cleanup needs your confirmation. ## Check and repair `agnostic-ai lint` and `doctor` flag a `MEMORY.md` over 100 lines, two `MEMORY.md` files over 6,000 bytes together, a broken link, a fact the list leaves out, text that looks like a secret, and hidden characters a reviewer cannot see. They skip personal memory when its folder is missing, so CI never checks it. `agnostic-ai memory lint` runs only these checks, and `agnostic-ai memory index` rebuilds each `MEMORY.md` after a merge conflict. See [memory commands](https://agnostic-ai.org/docs/cli-reference/memory/#memory) for every subcommand. ### Write or review a memory fact by hand A fact file: ```markdown --- name: ci-ubuntu description: PR CI runs on Ubuntu alone metadata: type: project --- PR CI runs on Ubuntu alone. **Why:** the full OS matrix costs too much to run on every push. **How to apply:** run the full matrix before merging a change to file paths. ``` `type` is `user`, `feedback`, `project`, or `reference`. The `sources:` setting does not move these folders. The `shared-memory-policy` rule asks tools to save only facts the code, history, and rules lack; to update a fact rather than add a duplicate; to check that named files and commands still exist; and to never save secrets. Review shared memory changes like code, because every tool reads them. ### How each tool loads memory and keeps its own notes ## How each tool loads it {#how-each-tool-loads-it} | Tool | How | | --- | --- | | Claude Code | `CLAUDE.md` imports the project list, and [Claude Code's own memory](#claude-code-s-own-memory) loads the personal list | | Codex, Copilot, Gemini CLI, Qoder, Factory | A command runs when a session starts and adds both lists | | Devin CLI (`windsurf`) | A SessionStart command adds both lists through tagged context; the rule remains a fallback | | Cursor local sessions and self-hosted workers | A command runs when a session starts and adds both lists | | Trae with project hooks enabled | A command runs when a session starts and adds both lists | | Cline VS Code | A project TaskStart hook returns both lists through `contextModification`; the shared rule remains the fallback | | Auggie CLI (macOS, Linux, Windows WSL) | An executable `SessionStart` script adds both lists | | OpenCode, Kilo Code | The config lists both files | | Kiro | A steering file includes project memory and, in checkout mode, personal memory | | Antigravity | An always-on rule includes project memory and, in checkout mode, personal memory | | Every other tool | A rule tells the tool to read both lists before a task | Kiro shows a marker for an included list that has no file yet, and the marker goes away once a tool saves a fact there. If you added the include to a steering file by hand, remove it. With `memory.personal: repo`, Kiro and Antigravity include only the project list and find personal memory with `agnostic-ai memory path`. With `sync --global`, both get only the rule, because a home file cannot name a project's lists. See [Kiro](https://agnostic-ai.org/docs/targets/kiro/#native-mappings) and [Antigravity shared memory](https://agnostic-ai.org/docs/targets/antigravity/#shared-memory), which covers its rule size limits. Aider gets only the rule, because a missing file shows an error. Once both `MEMORY.md` files exist, add them yourself in `.aider.conf.yml`: `read: [.agnostic-ai/local/memory/MEMORY.md, .agnostic-ai/memory/MEMORY.md]` ### When a session starts {#load-at-session-start} On Codex, Copilot, Cursor, Gemini CLI, Qoder, Factory, Trae, Devin CLI, and Auggie CLI on Unix, sync adds a hook that runs [`agnostic-ai hook memory`](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-memory) when a session starts. In Trae, enable project hooks in Settings > Hooks first. - It runs the project's `node_modules/.bin/agnostic-ai` when that file exists, and otherwise the `agnostic-ai` on your PATH. Trae runs the PATH one only. Keep the one it picks at 0.81.0 or newer with [`agnostic-ai upgrade`](https://agnostic-ai.org/docs/cli-reference/upgrades/#upgrade); an older one loads no memory, and with none found nothing loads. - Codex, Cursor, Gemini CLI, and Qoder run it only in a folder you trust. - On Windows, Cursor and Gemini CLI need `sh` on PATH, for example from Git Bash. The Claude Code subagent hook needs it too. - Hooks you wrote yourself stay. - It keeps the first 6,000 bytes, after dropping hidden characters. Facts past the cut never load on these tools, and `memory lint` names them (the first 10). Every other tool loads both lists whole, hidden characters included, so `memory lint` also warns there once they pass 6,000 bytes. Remove the hidden characters it reports. Cline VS Code gets a project-only `memory-cline-task-start` hook, which needs Cline for VS Code 4.1.20 or later with hooks enabled. Global sync leaves it out, because the Cline SDK and CLI ignore TaskStart hook replies. See [Cline VS Code memory startup](https://agnostic-ai.org/docs/targets/cline/#vs-code-memory-startup). Devin CLI uses the `memory-windsurf-session-start` hook with an explicit POSIX `sh` launcher and a 10-second timeout. It starts from `DEVIN_PROJECT_DIR` even when its process runs elsewhere. Native Windows hook execution is undocumented. Cursor's hosted cloud agents do not run `sessionStart` hooks or user-level hooks. They read the project index through the shared memory policy and skill. Local sessions and self-hosted workers run the hook. See [Cursor's cloud hook support](https://cursor.com/docs/hooks#cloud-agent-support). ### Claude Code's own memory {#claude-code-s-own-memory} Claude Code also saves facts by itself. Sync points that memory at your personal memory folder (`autoMemoryDirectory` in `.claude/settings.local.json`), so its saves land where every other tool reads them. Sync keeps a value you set yourself, and Git never commits the file. Claude Code uses the setting once you trust the folder. Claude Code then loads your personal list as its own memory: the first 200 lines or 25KB, with hidden characters cleaned out. `CLAUDE.md` imports only the project list. Claude Code's own memory never reaches a subagent, so a `SubagentStart` hook runs [`agnostic-ai hook memory`](https://agnostic-ai.org/docs/cli-reference/maintain/#hook-memory) to add the personal list. In a linked worktree that uses the main checkout's personal folder, the hook reads that same folder. `CLAUDE.md` imports both lists, and the hook adds nothing, in these cases: - Auto memory is off (`autoMemoryEnabled: false` in a settings file, or `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` in your shell or a settings `env` block). - Your own `autoMemoryDirectory` points to another folder. - A settings file sets `permissions.blockReadsOutsideWorkingDirectories: true` and the folder comes from repository-supplied settings (for example, a tracked local file or a local file outside Git). - `sync.unmanaged` prevents sync from setting the folder. Run `agnostic-ai sync` after you change these settings. Facts Claude Code saved before come along. When personal memory is still empty, the first sync that points Claude at it copies them from Claude's own folder (`~/.claude/projects//memory/`) and leaves that folder as it is. Otherwise `agnostic-ai lint` names the folder and how many of its facts no longer load (LINT043), and this copies them over: ```bash agnostic-ai memory import --from claude --dry-run agnostic-ai memory import --from claude ``` Every tool reads personal memory, so the dry run lists each fact with its description first; `--only ` copies just the ones you pick. `--path` copies from any folder with the same layout, such as Gemini CLI's `~/.gemini/tmp//memory/` or Qoder's `~/.qoder/projects//memory/`. See [memory import](https://agnostic-ai.org/docs/cli-reference/maintain/#memory). ### Each tool's own memory {#each-tools-own-memory} To share something a tool learned, ask it to save the fact in shared memory. Claude Code's own saves land there already, unless you set `autoMemoryDirectory` yourself. | Tool | Its own memory | Default | | --- | --- | --- | | Claude Code | Auto memory: notes for each repository, on your machine | On (`autoMemoryEnabled`); sync points it at your personal memory folder | | Codex | Memories: notes built from past sessions, one store for the machine | Off (`memories` under `[features]`) | | Gemini CLI | Edits its own Markdown memory files: `GEMINI.md` files and private notes for each project. Auto Memory proposes notes from past sessions. Sync generates the project's `GEMINI.md`, so the `gemini-memory-tiers` rule sends Gemini's team facts to shared memory instead, and has it copy private facts there too | Editing is always on, with no setting. Auto Memory is off (`experimental.autoMemory`) | | Copilot | Copilot Memory, hosted: repository facts and your preferences, dropped after 28 days unused. The VS Code memory tool: notes on your machine | Copilot Memory is on for individual plans; organization admins enable it first. CLI: `/memory on` or `/memory off`. The VS Code tool is built in | | Qoder | CLI Auto-Memory: notes the agent keeps on your machine. The IDE has a separate memory | CLI: off (`autoMemoryEnabled`). IDE: not documented | | Kilo Code | Kilo Memory: project notes outside the repository, no personal memory | Off (Settings > Context, or `/memory on`) | | Kiro | Memory: learned notes stored per user account, shared by the IDE and CLI | IDE: off (the user setting **Kiro Agent: Memory**; a workspace value has no effect). CLI: not documented (`/memories`, then `config`) | | Augment | Agent Memories in the IDE, which you approve one by one | Not documented | | Trae | Memories, global and for each project, as local Markdown files | Not documented | | Goose | Memory extension: files in `.goose/memory/` and `~/.config/goose/memory/` | Not documented (Extensions) | | Jules | Memory in the Jules cloud for each repository | Not documented (repository settings) | | Windsurf (Devin) | Devin Local keeps none. Cloud Devin has its own Memory | Cloud Devin Memory is on; whether it reaches Devin Desktop, Local, or the CLI is not documented | | Cursor | None documented | Not applicable | ### Share personal memory across linked worktrees ## Share personal memory across worktrees {#one-store-per-repository} By default each checkout has its own personal memory, so deleting a worktree deletes what it learned. A worktree Claude Code creates starts with a copy when sync manages `.worktreeinclude`; any other starts empty. In a worktree, Claude Code reads the main checkout's `.claude/settings.local.json`, so its own memory uses the main checkout's personal memory once sync has run there. To keep one personal memory for every worktree of a repository, add this to `agnostic-ai.local.yaml` and sync: ```yaml memory: personal: repo ``` For a new Git project, `agnostic-ai init --memory` enables shared memory and starts personal memory in this repo mode when managed ignores are on. It keeps an existing `agnostic-ai.local.yaml`. Without Git or with `--gitignore=off`, it enables memory in checkout mode. In checkout mode, sync names this option when linked worktrees exist. A linked worktree without its own `agnostic-ai.local.yaml` follows the main checkout's `memory.personal` setting; its own local file takes precedence. Other settings stay local to each checkout, and each teammate chooses their own mode. Personal memory then lives in `~/.agnostic-ai/local/memory/-/` (under `$AGNOSTIC_AI_HOME` when set). Sync creates it, readable only by you, and it works when `~/.agnostic-ai` is a link to another folder. Moving or recloning the repository starts a new folder. To see it: ``` $ agnostic-ai memory path project ~/code/app/.agnostic-ai/memory personal ~/.agnostic-ai/local/memory/app-fbcd84e8 ``` The first sync after a mode change copies facts into the new store when it holds no Markdown file, and prints the count. The old store stays unchanged. With facts already in the new store, `agnostic-ai lint` names the ones left behind; `agnostic-ai memory import --from checkout` copies them over (`--from repo` when you switch back). Dry runs, checks, and previews never copy facts. Later syncs keep imported facts you deleted out of the store; an explicit `memory import` can bring them back. That folder is outside the project, so some tools need its full path in their config. Sync writes the path only to files Git ignores, which needs the [`.gitignore` block](https://agnostic-ai.org/docs/configuration/#gitignore) (`gitignore.enabled: true`, which `agnostic-ai init` sets). The block lists those files, such as `.codex/config.toml`, whenever their tool and the `memory` built-in are on, so the committed `.gitignore` and `.worktreeinclude` stay the same for teammates without the setting, and a new Claude Code worktree copies them. If one of those files is already in Git, run `agnostic-ai sync --untrack`. To commit one instead, such as a hand-written `.codex/config.toml`, add its path to [`gitignore.allow`](https://agnostic-ai.org/docs/configuration/#gitignore). A file kept in Git through `gitignore.commit` or `gitignore.allow` never gets the path, so that tool works as without the block; `sync --gitignore off` does the same for one run. What each tool does with personal memory in this mode (project memory loads the same way in both columns): | Tool | Without the `.gitignore` block | With it | | --- | --- | --- | | Claude Code | Loads and saves | Same | | Copilot, Factory | Loads | Loads | | Codex, Gemini CLI, Qoder | Loads | Loads and saves | | Cursor | Loads | Loads; the CLI saves, the editor asks first | | OpenCode | Nothing | Loads and saves | | Windsurf | Finds it with `agnostic-ai memory path` | Same; Devin CLI saves without asking | | Kilo Code, Kiro, every other tool | Finds it with `agnostic-ai memory path` | Same | - **Saving outside the project.** Codex, Gemini CLI, Qoder, OpenCode, the Cursor CLI, and Devin CLI write only inside the project without asking, so sync gives them the folder: Codex `writable_roots` (read in `workspace-write` mode, in a trusted project), Gemini CLI `context.includeDirectories`, Qoder `permissions.additionalDirectories`, OpenCode `permission.external_directory` in `opencode.json`, and a `Write(/**)` rule in `.cursor/cli.json` and in `permissions.allow` of `.devin/config.json`. Every tool but Codex keeps your own entries. If your Codex overlay sets `[sandbox_workspace_write]`, sync leaves it alone; add the folder there yourself. - **Kilo Code** ignores project config entries outside the project. To load personal memory every session, add its `MEMORY.md` to `instructions` in `~/.config/kilo/kilo.jsonc`. - **Tools with only the rule** need `agnostic-ai` on PATH, and may ask before they read or write outside the project. - **Cloud agents** start from a fresh clone, so they never see personal memory. --- # Session handoffs The built-in `handoff` skill leaves a task note in one coding tool so you can resume the task in another on the same machine. The second tool checks the note against Git before it continues. ## Enable the skill Add this to `agnostic-ai.yaml`, then run `agnostic-ai sync`: ```yaml requires: ">=0.80.0" builtins: [handoff] ``` Projects created with `init` have it on already. For every project, add the same lines to `~/.agnostic-ai/agnostic-ai.yaml` and run `agnostic-ai sync --global`. Most tools get a skill; tools set to `emit-skills-as-commands` also get a command. Aider gets the text inline when `outputs.aider.rules-file` is set. Jules has no skill support. ## Leave a session Ask the current tool to “write a handoff”, or “write a handoff for feat/x” for another branch. It saves the goal, progress, and next steps in a short note for the branch. ## Resume in another tool Open the other tool in the same project and say “resume the handoff”. It finds the newest note for the branch, reports the next steps, and checks the branch, commit, and working changes against Git. Unexpected differences need your confirmation. ## Limits and overrides Handoffs stay on one machine; teammates never see them. Linked worktrees (extra checkouts of the same repository) can share the notes, which Git ignores. List saved tasks with [`agnostic-ai handoff list`](https://agnostic-ai.org/docs/cli-reference/maintain/#handoff-list). To replace the built-in, create your own project skill named `handoff`; `agnostic-ai list` shows which one wins. If a global built-in clashes with your project skill, rename yours or turn the built-in off in the global config. ### Inspect note format, branch matching, and fallback stores ```markdown # Handoff tool: codex | date: 2026-10-05T14:02Z | branch: feat/x | head: abc1234 worktree: /path/to/checkout | status: active | pr: none ## Goal ## Acceptance ## Done ## In progress ## Next steps ## Open decisions ## Gotchas ## Verify ``` Notes live in `.agnostic-ai/local/handoffs/` in the main checkout. `feat/x` becomes `feat-x.md`: any character other than a letter, number, `.`, `_`, or `-` becomes `-`. A note replaces only its branch's previous note; a filename collision needs confirmation. Status is `active`, `blocked`, or `ready`. Writes go through a private temporary file and an atomic rename, with mode 0600. Notes exclude secrets and native tool memory. The project root is the nearest folder with `agnostic-ai.yaml`, or the Git repository root when there is none. A subproject with its own config keeps its own store at the same relative path in the main checkout. If that checkout is bare, its store is not ignored by Git, or a write fails, the skill uses the current checkout's ignored store. It stops if neither is safe. From the main checkout, ask to resume a named branch. If no note matches the current branch, the skill lists local task headers and asks which to resume, the same tasks `agnostic-ai handoff list` shows. A task whose worktree is gone shows as removed, and its note stays available. A tool in the task's live worktree resumes its note without a branch mismatch prompt. The old `.agnostic-ai/local/HANDOFF.md` and `HANDOFF.auto.md` still work. The skill finds matching automatic snapshots in the shared store, the checkout store, and the old path. When the newest snapshot is newer than the manual note, it adds that Git evidence and keeps the note's goal, next steps, status, and PR. A shared snapshot survives removal of its worktree. ### Write notes from linked worktrees ## Write from a linked worktree Sync can grant access to the main checkout's ignored handoff store: | Tool | Native setting | |---|---| | Codex | `sandbox_workspace_write.writable_roots` | | Claude Code | `permissions.additionalDirectories` in `settings.local.json` | | Qoder | `permissions.additionalDirectories` | | Factory | `sandbox.filesystem.allowWrite` in `.factory/settings.local.json` | | OpenCode | `permission.external_directory`, `"/**": "allow"` | | Devin CLI | `permissions.allow`, `Write(/**)` | The absolute path goes only into native config that Git ignores. For a tracked file, sync reports the path and asks you to untrack the file; for a hand-written or unmanaged file, it reports the access to add yourself. Existing restrictions still apply, Factory's grant does not turn its sandbox on, and an explicit Codex sandbox table in an imported overlay stays unchanged. OpenCode and Devin grants go only into files sync created or granted in before; skipping a grant does not skip the rest of the file's sync. OpenCode keeps any existing rule for that path and gets no grant when the path contains glob characters. Hand-written grants always stay. An unchanged generated grant leaves with the built-in, including one in a file you later committed. An unsafe main store (tracked, not ignored, a symlink, or inaccessible) gets no grant, and the skill uses a safe store in the current checkout. Dry runs and checks create no store. ### Keep useful lessons after a handoff ## Save what you learned The lead asks each delegate to write a handoff for its branch before returning. After a handoff, the skill suggests lessons worth keeping, each with its scope and exact change, and saves only the ones you approve. | Scope | File | Then run | | --- | --- | --- | | [project] | `.agnostic-ai/rules/learnings.md` | `agnostic-ai sync` | | [personal] | `.agnostic-ai/local/rules/learnings-local.md` | `agnostic-ai sync` | | [global] | `~/.agnostic-ai/rules/learnings.md` | `agnostic-ai sync --global` | Project lessons are shared in Git; personal lessons stay in the checkout; global lessons apply across projects. The project file follows `sources.rules` if you changed it, and the global file follows `AGNOSTIC_AI_HOME` if set. If `agnostic-ai` is not on your PATH, the skill prints the sync command for you to run. ### Save automatic Git snapshots and show startup notices ## Add automatic Git snapshots The separate `handoff-hook` built-in, off by default, saves the Git state when a session ends or the tool shortens its conversation history (context compaction): ```yaml requires: ">=0.80.0" builtins: [handoff, handoff-hook] ``` | Tool | Saves a snapshot on | Shows a notice on | | --- | --- | --- | | [Claude Code](https://code.claude.com/docs/en/hooks) | `PreCompact`, `SessionEnd` | `SessionStart` | | [Codex](https://learn.chatgpt.com/docs/hooks) | `PreCompact`, `SessionEnd` | `SessionStart` | | [Gemini CLI](https://geminicli.com/docs/hooks/reference/) | `PreCompress`, `SessionEnd` | `SessionStart` | | [Qoder CLI](https://docs.qoder.com/cli/hooks-reference.md) | `PreCompact`, `SessionEnd` | `SessionStart` | | [Factory / Droid](https://docs.factory.com/harness/hooks) | `PreCompact`, `SessionEnd` | `SessionStart` | The snapshot goes to `handoffs/.auto.md` in the same store as manual notes, written the same safe way, with the same fallback to the checkout store. It holds the tool, date, branch, HEAD, absolute worktree path, `git status --short`, and the last five commit messages. Each hook also keeps `.agnostic-ai/local/HANDOFF.auto.md` as a compatibility copy when that path is ignored. It never reads your handoff note, diffs, transcripts, or tool memory. Outside a git repository, or before the first commit, no snapshot is saved. When a session starts in a linked worktree, the notice names its branch's handoff. In the main checkout, it reports how many branches have handoffs. A manual note and a snapshot for the same branch count as one task, and the notice prefers the manual note. Snapshot worktree headers end with `detached: true` or `detached: false`. A detached snapshot uses its HEAD as the task key, so different detached tasks and a branch named `detached` stay separate. The final boolean keeps marker-like path text intact. The hooks still show the old messages for `.agnostic-ai/local/HANDOFF.md` and `HANDOFF.auto.md` when no task handoff matches. Nothing resumes until you ask. Claude hooks use the working directory (`cwd`) from the tool's event, including in linked worktrees, even when the hook process runs in the main checkout. Config discovery stays inside that checkout. Empty input or an event without `cwd` uses the process directory. Invalid or ambiguous input saves nothing. Input is limited to 64 KiB, 32 nested containers, and a two-second read deadline. Claude hooks need Bash; the other hooks need `sh`. All need Git and `awk`. On Windows, use Git for Windows with its Unix tools on PATH. Start Claude Code from the project root so it loads `.claude/settings.json`. Each tool's own hook settings still apply: approve Codex hooks with `/hooks`, and approve Gemini project hooks when asked. A tool may stop before the snapshot finishes when it exits or compacts. ### Factory notices Factory hides hook output by default. To see the startup notice, add this settings spec and sync: ```yaml # .agnostic-ai/settings/handoff-notices.yaml name: handoff-notices targets: [factory] x-factory: showHookOutput: true ``` Factory hooks only work per project, not through `sync --global`. ### Global hooks Claude Code, Codex, Gemini CLI, and Qoder CLI can also get the hooks through `agnostic-ai sync --global`. They still find the current project at run time. A project `agnostic-ai sync` adds `.agnostic-ai/local/` to the ignore file. With only global hooks or skills, add that line to the project's `.gitignore` yourself. ## See where it comes from ```bash agnostic-ai list agnostic-ai explain builtin:handoff agnostic-ai why .claude/skills/handoff/SKILL.md ``` Reports show `builtin:handoff` and its release version. After an upgrade that changes the built-in, sync again. --- # Use RTK and Caveman with agnostic-ai [RTK](https://github.com/rtk-ai/rtk) shortens the output of common terminal commands before the coding agent reads it. [Caveman](https://github.com/JuliusBrussee/caveman/tree/main/skills/caveman) is a skill that asks the agent to answer in fewer words. agnostic-ai can set up either one. Use RTK to shorten command output, or Caveman for shorter answers; our small test found no extra cost saving from combining them (see [test results](#test-results)). ## Turn them on Add the names to `builtins` in `agnostic-ai.yaml`, next to any you already use: ```yaml version: 1 targets: [claude] builtins: [rtk] ``` Both are off by default; use `builtins: [rtk, caveman]` for both. Then run: ```sh agnostic-ai sync ``` - **RTK** adds a hook to `.claude/settings.json`. A hook is a command Claude Code runs before each shell command. It works only in Claude Code, and only for the Bash tool on macOS or Linux, not PowerShell. - **Caveman** adds one file, `SKILL.md`, to `.claude/skills/caveman/` and to every other tool that supports skills. Type `/caveman` to start it. Caveman's license and notices are in its [upstream repository](https://github.com/JuliusBrussee/caveman). agnostic-ai does not install either program. Install RTK with its [install guide](https://github.com/rtk-ai/rtk#installation); without it, commands run as normal. To preview how RTK changes a command without running it, use `agnostic-ai doctor rtk --command 'git status'`. ## What happens on a command ```text Claude asks to run: git status RTK changes it to: rtk git status Claude checks its permission rules on the new command RTK runs it and returns shorter output ``` Because Claude checks the changed command, an "ask first" rule for `git status` needs a twin for `rtk git status`. See the [tested permission cases](https://github.com/Chemaclass/agnostic-ai/blob/main/docs/internal/experiments/trio-live-hooks.md). ## If you already use RTK or Caveman Keep only one copy. If RTK is already on for all your projects, two hooks can run for the same command, so leave the built-in off or remove your own copy first. If your project already has a hook exactly like the RTK one, agnostic-ai takes it over, and removing `rtk` later deletes it. Save a copy first if you want to keep it. ## Turn them off - Say `stop caveman` to get normal answers again in this session. - Remove the name from `builtins` and run `agnostic-ai sync`. Use `builtins: []` to turn off all built-ins. - `disabled: true` has no effect on the RTK hook. A copy you installed yourself, outside agnostic-ai, needs its own removal. ## Share with a team The [example packs](https://github.com/Chemaclass/agnostic-ai/tree/main/docs/examples/rtk-and-caveman) offer RTK and Caveman as separate [packs](https://agnostic-ai.org/docs/packs/) instead of built-ins. Use a pack or a built-in for each tool, not both. ### Distinguish the Caveman program and read the test results ## Caveman's output shrinking is separate Caveman can also run as a program that shortens command output. The `caveman` built-in does not turn that on. In our test, RTK cut one failing test log from 23,865 bytes to 240 bytes, and Caveman cut nothing more. See the [details](https://github.com/Chemaclass/agnostic-ai/blob/main/docs/internal/rtk-caveman-runtime.md) before using both programs together. ## Test results We ran 24 short debugging sessions in Claude Code 2.1.295 with RTK 0.51.0 and `claude-haiku-5-5`: | Turned on | Cost on larger tasks | Speed | | --- | --- | --- | | RTK | Lowest | Slower in 4 of 6 runs | | Caveman | No saving | Slower in all 6 | | Both | More than RTK alone | Slower in all 6 | All 24 answers were correct. On a short command RTK does not shorten, RTK did not help. These are price estimates from a small test, not your bill, so measure a full task in your own project before you choose. See the [full results](https://github.com/Chemaclass/agnostic-ai/blob/main/docs/internal/experiments/trio-evaluation-results.md) and the [investigation](https://github.com/Chemaclass/agnostic-ai/issues/1957). --- # Git hooks Git hooks run agnostic-ai at Git events such as a commit or checkout, so stale generated files are caught before CI. They are separate from [coding-agent hooks](https://agnostic-ai.org/docs/spec-format/hooks/), which run inside a coding tool. Pick a recipe by whether your team [commits or ignores outputs](https://agnostic-ai.org/docs/getting-started/#commit-or-ignore-generated-outputs): - Committed outputs: [check the staged files](#check-staged-files) at commit time, so you never commit output you forgot to stage. - Ignored outputs: run `sync` once in each checkout, then [regenerate on checkout and pull](#regenerate-on-checkout) so coding agents find their files. In CI, use the [ignored-output recipe](https://agnostic-ai.org/docs/ci/#ignored-outputs). ## Install the project hooks ```bash agnostic-ai install-hook agnostic-ai install-hook --post-checkout ``` The first command installs the commit check. The second regenerates files after checkout and pull. Install once per checkout. If your project uses a hook manager, use a recipe below instead of replacing its hooks. ### Use pre-commit, lefthook, or husky instead ## pre-commit (Python) [pre-commit](https://pre-commit.com) works in projects that use several languages. Add `.pre-commit-config.yaml`: ```yaml repos: - repo: local hooks: - id: agnostic-ai-check name: agnostic-ai sync --check entry: agnostic-ai sync --check language: system pass_filenames: false files: '^(\.agnostic-ai/|agnostic-ai\.yaml$|agnostic\.config\.yaml$)' ``` Then install it: ```bash pre-commit install ``` `files:` runs the hook only when a spec changes. `pass_filenames: false` checks the whole project, like CI. ## lefthook [lefthook](https://lefthook.dev) is a single Go binary. This repo uses it (see [`lefthook.yml`](https://github.com/Chemaclass/agnostic-ai/blob/main/lefthook.yml)). Add to `lefthook.yml`: ```yaml pre-commit: commands: agnostic-ai-check: glob: "{.agnostic-ai/**,agnostic-ai.yaml}" run: agnostic-ai sync --check ``` Then install it: ```bash lefthook install ``` `glob:` runs the hook only when a spec or the config is staged. ## husky + lint-staged In Node projects, use [husky](https://typicode.github.io/husky) with [lint-staged](https://github.com/lint-staged/lint-staged). `package.json`: ```json { "scripts": { "prepare": "husky" }, "lint-staged": { "{.agnostic-ai/**,agnostic-ai.yaml}": "agnostic-ai sync --check --against index --" } } ``` `.husky/pre-commit`: ```sh npx lint-staged ``` Then install it: ```bash npm install ``` The trailing `--` keeps the file paths lint-staged adds from being read as options. `sync --check` checks the whole project, not single files. ## Check the staged files {#check-staged-files} A plain `sync --check` reads the working tree. If `sync` rewrote `AGENTS.md` and you did not stage it, the check passes and you commit a stale file. `--against index` compares the staged specs with the staged outputs instead: ```bash agnostic-ai sync --check --against index ``` A staged spec change without its regenerated output fails. The error names the file and tells you to run `agnostic-ai sync` and `git add` the output (for `--against HEAD`, commit it). The hook from `agnostic-ai install-hook` runs this form; run `install-hook` again to update a hook an older version wrote. With `gitignore.enabled: true`, the hook checks only that the staged specs render, because `--against index` compares only outputs Git tracks. A plain `sync --check` still compares every output. ### Limits of --against - `--against` needs agnostic-ai 0.74.0 or later. A `--shared` hook blocks commits for a teammate on an older release until they upgrade. - The check copies the tracked files to a temporary folder on each commit. In a large repository, use the lint-staged recipe above so it runs only when specs are staged. - `--against HEAD` checks the last commit instead. See the [CI recipe](https://agnostic-ai.org/docs/ci/#committed-outputs). - For what the check compares and skips, see [`--against` details](https://agnostic-ai.org/docs/cli-reference/sync/#against-details). If your hook manager runs a command only when matching files are staged, the glob must list every input, including a README that a review pulls in with `@path`. `agnostic-ai explain --inputs` prints them, one per line: ```bash agnostic-ai explain --inputs # .agnostic-ai/** # .gitignore # agnostic-ai.local.yaml # agnostic-ai.yaml # apps/web/README.md ``` Paste the list into the glob, or drop the glob and check every commit. Rerun it after you add an `@path` line. ## Regenerate on checkout and pull {#regenerate-on-checkout} With ignored outputs (`gitignore.enabled: true`), a fresh clone or new `git worktree` has no `CLAUDE.md`, rules, or hooks until someone runs `sync`, so an AI session started there by automated worktree creation finds no config. A `post-checkout` hook fixes this: `git checkout`, `git clone`, and `git worktree add` all run it. A pull that merges runs `post-merge` instead, which also restores generated files that an incoming commit untracks. `agnostic-ai install-hook --post-checkout` writes both hooks. They prefer the local binary and run `agnostic-ai project` from the worktree root, which keeps manual edits and installs no dependencies. They skip a checkout without `agnostic-ai.yaml`, and print a recovery command when the binary is missing. Run the command again in a clone with older generated hooks to update them and add pull coverage. To write the hooks yourself, use [`sync --keep-edits`](https://agnostic-ai.org/docs/cli-reference/sync/#sync), which writes every other output and leaves each file you edited since the last sync in place, printing `~ kept `. To keep such an edit, move it into `.agnostic-ai/` and run `agnostic-ai sync`. lefthook (`lefthook.yml`): ```yaml post-checkout: commands: sync: run: agnostic-ai sync --keep-edits ``` Plain git (`.git/hooks/post-checkout`, `chmod +x`): ```sh #!/bin/sh agnostic-ai sync --keep-edits ``` To run only on branch and worktree switches, check the third argument, which is `0` for a file checkout: ```sh #!/bin/sh [ "$3" = "1" ] || exit 0 # 1 = branch checkout, 0 = file checkout agnostic-ai sync --keep-edits ``` The same command works in `post-merge` (without the `$3` check, which it does not get) and in a `postinstall` script. A pull with rebase needs a `post-rewrite` hook (see [Node monorepos](#node-monorepos)). `agnostic-ai` must be on `PATH` wherever the repo is checked out; document the install in `CONTRIBUTING.md` so a new contributor does not hit `command not found`. If some contributors lack the CLI, commit the generated outputs instead, or, in a Node project, install the CLI with the dependencies (see [Node monorepos](#node-monorepos)). ### Pin the CLI and restore files in Node workspaces and worktrees ## Node monorepos {#node-monorepos} In a pnpm workspace with ignored outputs, `pnpm install` creates every tool file, and git hooks regenerate them on each branch switch. npm and Yarn workspaces work the same way. ### Pin the CLI Add the npm package to the workspace root as an exact dev dependency: ```bash pnpm add -D -E -w agnostic-ai # pnpm workspace npm install -D -E agnostic-ai # npm; or yarn add -D -E, bun add -D -E ``` The lockfile covers every OS and CPU, so one pin works on macOS, Linux, and Windows. Also set `requires: ">=X.Y.Z"` in `agnostic-ai.yaml`, so an older installed version stops with [AAI-005](https://agnostic-ai.org/docs/errors/#aai-005-installed-version-outside-requires) before rewriting files. If you commit generated files, use an exact `requires: "X.Y.Z"` instead, since a newer release can write different output. [Requires](https://agnostic-ai.org/docs/configuration/#requires) also accepts ranges. ### Sync on install Root `package.json`: ```json { "scripts": { "postinstall": "agnostic-ai project" } } ``` Package scripts find the pinned binary in `node_modules/.bin`. Cloud agents and IDE worktree setups (Cursor Cloud, Codex environments, `.cursor/worktrees.json`) run `pnpm install` but no git hook, so this step is how they get their tool files. ### Sync on checkout, pull, and rebase `lefthook.yml`: ```yaml post-checkout: commands: agnostic-ai-sync: run: node_modules/.bin/agnostic-ai project post-merge: commands: agnostic-ai-sync: run: node_modules/.bin/agnostic-ai project post-rewrite: commands: agnostic-ai-sync: run: node_modules/.bin/agnostic-ai project ``` `post-merge` runs after `git pull`, and `post-rewrite` after a rebase or `git commit --amend`. Git hooks do not put `node_modules/.bin` on `PATH`, so these commands name the binary by path. If it is missing, run the package install, or a global `agnostic-ai project --bootstrap` with a version that supports the command. `--quiet` hides the summary but still reports `~ kept `. ### First pull When a branch changes the pinned version, `agnostic-ai project --check` reports the installed and required versions without installing anything, and `agnostic-ai project --bootstrap` installs from the lockfile once, then syncs with `--keep-edits --quiet`. Use these instead of comparing manifest text in shell. A hook manager can call `agnostic-ai project --check --against index` for a read-only pre-commit gate. See [`project`](https://agnostic-ai.org/docs/cli-reference/project/#project) for version checks, supported package managers, and the releases that lack the command. Generated hooks first check that the selected binary supports `project`. When no `agnostic-ai` is on `PATH`, they name the install to run, for example `bun install --frozen-lockfile`. Built-in memory hooks look for the nearest ancestor config before selecting a binary, including projects nested inside another Git checkout. ### New worktrees A new linked worktree has no `node_modules`, so the pinned binary is missing and a hook runner installed from npm, such as lefthook, may not start. The worktree gets its tool files once `pnpm install` runs in it. Put the install in an environment spec, `.agnostic-ai/environments/dev.yaml`: ```yaml name: dev setup: agnostic-ai project --bootstrap ``` This needs a global agnostic-ai that supports `project`, installed before worktree setup. Codex and Cursor run `setup` in a new worktree from their own files. For Claude Code, sync writes hooks that run it once in each new worktree. Delete any hand-written `bootstrap.yaml` hook from an earlier version of this recipe, because Claude Code runs matching hooks in parallel. See [Claude Code worktree setup](https://agnostic-ai.org/docs/spec-format/environments/#claude-code-worktree-setup). Claude Code copies the gitignored files that `.worktreeinclude` lists into each new worktree. When `claude` is a target, sync keeps its block there (see [gitignore](https://agnostic-ai.org/docs/configuration/#gitignore)), so the ignored `.claude/settings.json` and the setup script reach the worktree. Otherwise, commit both so a new worktree has them before any sync runs. In `agnostic-ai.yaml`: ```yaml gitignore: enabled: true commit: [claude:environments] ``` Commit the files after `sync` writes them. A fresh install writes the same content, so `git status` stays clean. ### Workspace settings `pnpm-workspace.yaml`: ```yaml allowBuilds: lefthook: true minimumReleaseAge: 1440 minimumReleaseAgeExclude: - agnostic-ai - '@agnostic-ai/*' ``` - `allowBuilds` lets lefthook's install script add the git hooks. pnpm 11 fails the install when a dependency's build script is not approved. pnpm 10 calls the setting `onlyBuiltDependencies`. - `minimumReleaseAge` needs both exclusions, because the binary ships in separate `@agnostic-ai/-` packages and the cooldown holds back each one on its own. ### Formatters Put ignore patterns in a fenced code block. Prettier escapes `*` in plain Markdown text (`/res/*` becomes `/res/\*`) but leaves code blocks alone. See [ignore specs](https://agnostic-ai.org/docs/spec-format/ignore/). Keep formatters away from committed generated files, or each sync undoes their edits. Prettier already skips files that `.gitignore` lists. `.prettierignore`: ```text .claude/settings.json ``` To skip a hook for one commit, use `git commit --no-verify`. Save it for emergencies. --- # Compare targets Pick two AI coding tools, and optionally a third in Target C, to see the files agnostic-ai writes for each, grouped by spec kind. The URL keeps your choice, so you can share it. For example, with Claude Code and Codex, a shared rule becomes a Claude rule file and a Codex `AGENTS.md` section. This page shows sample output paths. Before switching tools in an existing project, run [`agnostic-ai compare`](https://agnostic-ai.org/docs/cli-reference/inspect/#compare) to check the fields in your own specs. ## Read the paths with care Paths come from a real sync of one sample spec per kind. A file that holds several kinds, such as `.claude/settings.json`, appears under each. A listed file does not mean the tool reads it: Codex project commands can be written through a legacy opt-in, but Codex does not load them. Open a target page for field limits and optional outputs. --- # Error codes Each agnostic-ai error starts with an `[AAI-NNN]` code and ends with its fix. Find the code below for the cause. For a symptom without a code, use [Troubleshooting](https://agnostic-ai.org/docs/troubleshooting/). ``` [AAI-003] read config: no agnostic-ai.yaml or agnostic.config.yaml in /path/to/project fix: Run `agnostic-ai init` to scaffold a config, or `cd` into the directory that already contains one. Run `agnostic-ai doctor` for a full diagnosis. ``` `agnostic-ai explain AAI-003` prints the same cause and fix as the entry below. Pass `--json` for JSON output. ## Numbering | Range | Area | | --------- | -------------------------- | | `001-099` | reading specs and config | | `100-199` | writing output (collisions, hooks) | | `200-299` | import | | `300-399` | sync and validate | Codes keep their number across releases. New codes are added at the end. ## Codes ### AAI-001: Spec parse failed A spec file, or a review `@path` include, could not be read. Markdown specs use YAML frontmatter; hooks and MCPs are plain YAML. The error gives the path and, when known, the line and column. **Fix:** at the reported position, check that `---` lines wrap the frontmatter and that the YAML is valid: correct indentation, no tabs, quotes where needed. For an include, create the file or use a path inside the project. ### AAI-002: Spec kind not supported by target Your project has a spec kind (hook, mcp, command, ...) that the tool does not support. This is a warning by default, and a failure with `on-unsupported: error`. **Fix:** remove the spec, use a tool that supports the kind, or set `on-unsupported: warn` (or `silent`) in `agnostic-ai.yaml`. ### AAI-003: Config file missing Neither `agnostic-ai.yaml` nor the legacy `agnostic.config.yaml` exists in the project root. **Fix:** run `agnostic-ai init` to create a config, or `cd` into the directory that already contains one. Run `agnostic-ai doctor` for a full diagnosis. ### AAI-004: Config decode failed The config file is not valid YAML, has keys the config does not accept, or holds a value of the wrong shape. Each finding names its file, line, and dotted path, such as `agnostic-ai.yaml:4: unknown key "sync.collsion-policy" (did you mean collision-policy?)` or `agnostic-ai.yaml:2: gitignore: expected a map, got the string "yes-please"`. A mistake in `agnostic-ai.local.yaml` names that file and its own line. A `version` other than `1` fails here too, as does a [`requires`](https://agnostic-ai.org/docs/configuration/#requires) value that is not a version constraint, such as `latest` or `>=0.73.0,<0.74.0`. **Fix:** go to the file and line the message names. Give the key the shape it expects, rename or remove an unknown key (using its did-you-mean suggestion when there is one), or set `version: 1`. Check indentation, or check the file against `docs/schemas/config.schema.json`. Run `agnostic-ai doctor` for a full diagnosis. ### AAI-005: Installed version outside requires The installed binary is outside the releases the config's [`requires`](https://agnostic-ai.org/docs/configuration/#requires) key allows. An exact pin blocks newer releases too. Every command that reads the specs stops before it reads them or writes files, including `sync`, `lint`, `validate`, `doctor`, `revert`, and `cleanup`. The message names the config files, the required version, and the installed one: ``` [AAI-005] agnostic-ai.yaml + agnostic-ai.local.yaml requires agnostic-ai 0.88.0, but 0.89.0 is installed. The installed release is newer than this project allows. fix: run `agnostic-ai upgrade --requires` to adopt installed agnostic-ai 0.89.0 This updates requires and the schema URL, then runs sync. To accept newer releases when generated files are git-ignored, set `requires: ">=0.88.0"` in the file that pins it. ``` **Fix:** to adopt a newer installed release, run `agnostic-ai upgrade --requires` from the project root (through your package manager for a project install, such as `pnpm exec agnostic-ai upgrade --requires`). It raises `requires` to the installed release and keeps its form, so a minimum stays a minimum and an exact release stays exact. A range becomes a minimum. It also updates the schema tag, then syncs. Running `upgrade` or `update` alone updates the installed tool. Pass `--requires` to adopt an already-installed release in this project. When generated files are git-ignored, set `requires: ">=X.Y.Z"` instead of an exact release, so later releases keep working. Run `agnostic-ai explain AAI-005` for the full list of fixes. - **Installed release is older:** run `agnostic-ai upgrade`, or `upgrade --version vX.Y.Z` for an exact pin or range. For a package-manager install, the message names the install command, such as `pnpm install`. - **You want to keep an older pin:** install the release it names through your package manager or `upgrade --version`, which also downgrades a standalone binary. - **Global home config:** edit `requires` in the named file to adopt the installed release. `upgrade --requires` changes project config only. - **The version does not change:** `agnostic-ai upgrade --check` shows which binary runs and any older copy that shadows it on PATH. In an interactive terminal, a project version mismatch also offers a menu: ```text Choose a next step: 1. Apply suggested fix (update project requires and schema, then sync) 2. Open Claude Code with a fix prompt 3. Open Codex with a fix prompt 4. Show the fix prompt to copy 0. Exit Choice [0]: ``` The apply option appears when the installed release is newer than the project allows. Selecting it runs `upgrade --requires`. Review the changed files and rerun your original command. Claude Code and Codex appear when they are in the project's merged `targets` and installed on `PATH`. Selecting one shows the prompt and opens an interactive session in the project directory with the error and suggested fix. The tool keeps its normal permissions. Use the copy option for another tool. Enter or `0` exits without taking action. Every option keeps the original error status, because the failed command does not run again; a chained command such as `agnostic-ai sync && git push` stops either way. After the fix, rerun your original command. The menu is skipped for redirected input or output, CI, quiet or JSON output, global commands, checks, previews, watch mode, hooks, and editor services. ### AAI-102: Tools write to the same file {#aai-102-targets-emit-to-the-same-output-path} Two or more enabled targets would write different content to the same path, so sync stops instead of letting the last one win. **Fix:** drop one of the colliding targets from `targets:` in agnostic-ai.yaml, or override the matching `outputs.` path setting, such as `file`, `rules-file`, or `skills-dir`. ### AAI-103: Hand-written ignore file would lose patterns {#aai-103-hand-authored-ignore-file-would-lose-patterns} Replacing a tool's ignore file that has no agnostic-ai header would remove or reorder patterns, or add a negation. That can make excluded files readable, so sync refuses. The error names a risky negation or up to five missing or reordered patterns, with a count for the rest. **Fix:** run `agnostic-ai import ` to copy the file's patterns into an ignore spec. When a tool reads several ignore files, combine their patterns in the spec, in the same order, before syncing. Deleting the file also clears the error, at the cost of those patterns. See [ignore overwrite behavior](https://agnostic-ai.org/docs/spec-format/ignore/#overwrite-behaviour). ### AAI-202: Import source name unknown The tool name passed to `agnostic-ai import` is not supported. **Fix:** run `agnostic-ai import --help` for the supported list, and check the spelling. ### AAI-203: Import would replace an existing spec `import`, `init --from`, or `use` would replace a spec with content the importing tool never read: a hand-written spec, one edited since the last sync or import (a comment counts), one the last sync never wrote for that tool, or one another tool's import wrote. It also stops when a tool edit would replace a spec that holds `::target` blocks, or a key or comment it could not carry back. The import writes no spec, and `import --dry-run` fails the same way. The message lists each spec, the tool its current content came from, the tool that wanted it, and what the spec would lose. **Fix:** rename the existing spec to keep both and import again, or run `agnostic-ai import --overwrite` to replace it. When the message names what the spec would lose, make the tool's edit in the spec instead to keep it. ### AAI-301: Unknown sync target A target named in `--target`, `--only`, or the config is not built in, and no `agnostic-ai-adapter-` binary is on PATH. A name passed with `--target` fails the run. A config target that does not resolve only warns, so a teammate without an external adapter can still sync. That sync keeps the missing target's files and prints `~ skipped removing stale files: did not load; a sync that loads every target removes them`. When the name is close to a built-in name, the message suggests it: `unknown target: claud (did you mean claude? no agnostic-ai-adapter-claud on PATH)`. A known target that is not in this run (`sync --only codex` with `targets: [claude]`) reports `codex is not in this run's targets (claude)`. **Fix:** check the spelling. If the target exists but is not in this run, add it to `targets` or pass it with `-t`. Use the names in [Supported tools](https://agnostic-ai.org/docs/targets/). External adapters live on PATH as `agnostic-ai-adapter-`. ### AAI-302: Mutually exclusive flags You passed two conflicting flags, such as `--only` with `--except` or `--watch` with `--check`, or a flag without the one it needs, such as `--diff` without `--dry-run`. **Fix:** the message names both flags. Drop one when they conflict, or add the missing one. --- # Verify a release The install scripts and `agnostic-ai upgrade` already check each download against the release checksum, and against its [build provenance](#build-provenance) when the GitHub CLI is installed and logged in. Use the checks below for a manual download, or for proof of where a release came from. Replace `vX.Y.Z` and the archive filename with your release and platform. | Check | Proves | Command | |---|---|---| | Checksum | The archive matches the release's `checksums.txt`. | `sha256sum -c` | | Proof of where the archive was built | This repository's release workflow built the archive from the tagged commit. | `gh attestation verify` | | List of included modules (SBOM) | Which Go modules and versions are in the binary. | `.sbom.json` | | Proof of where the npm package was built | The npm package came from this repository's release workflow. | `npm audit signatures` | | Signed tag | The maintainer signed the release commit and tag. | `git verify-tag` | ### Check a downloaded archive ## Checksums `install.sh`, `install.ps1`, and `agnostic-ai upgrade` stop on a checksum mismatch, a missing `checksums.txt`, or a missing SHA-256 tool. To check by hand: ```bash curl -fsSLO https://github.com/Chemaclass/agnostic-ai/releases/download/vX.Y.Z/agnostic-ai_linux_amd64.tar.gz curl -fsSLO https://github.com/Chemaclass/agnostic-ai/releases/download/vX.Y.Z/checksums.txt grep ' agnostic-ai_linux_amd64.tar.gz$' checksums.txt | sha256sum -c # macOS: shasum -a 256 -c ``` The checksum comes from the same release page. It catches a corrupted download, not a compromised release. ### Verify where the archive was built ## Check where the archive was built {#build-provenance} Releases after 0.74.0 include a signed record of where every archive and module list was built, called a [build provenance attestation](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations). The release workflow signs it through Sigstore without keeping a permanent signing key. Verify it with the [GitHub CLI](https://cli.github.com/): ```bash gh attestation verify agnostic-ai_linux_amd64.tar.gz --repo Chemaclass/agnostic-ai ``` A pass means a workflow in `Chemaclass/agnostic-ai` built the file from a tagged commit. To check against the release's `agnostic-ai.intoto.jsonl` instead of GitHub's API: ```bash gh attestation verify agnostic-ai_linux_amd64.tar.gz --repo Chemaclass/agnostic-ai --bundle agnostic-ai.intoto.jsonl ``` `install.sh`, `install.ps1`, and `agnostic-ai upgrade` run this check whenever `gh` is installed and logged in, require the attestation to come from `.github/workflows/release.yml` at the release tag, and stop when it fails. Without `gh`, with a `gh` that is logged out or too old to verify, or for a version you name from before 0.75.0, they say so in one line and install on the checksum alone. They refuse a latest release from before 0.75.0, since whoever can replace release files could publish one. `AGNOSTIC_AI_VERIFY_ATTESTATION` changes this for all three: `1` stops when the check cannot run (`-VerifyAttestation` for `install.ps1`), and `0` skips it. ### Inspect the included modules ## List of included modules {#sbom} Each archive has a software bill of materials (SBOM) in SPDX format, such as `agnostic-ai_linux_amd64.tar.gz.sbom.json`, that lists every Go module in the binary. Its checksum is in `checksums.txt`, and it has its own signed record of where it was built. Scan it with a tool such as `grype sbom:agnostic-ai_linux_amd64.tar.gz.sbom.json`. ### Verify npm packages ## npm The `agnostic-ai` package and its six `@agnostic-ai/*` platform packages are published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements), a signed record of where they were built. In a project that installs them: ```bash npm audit signatures ``` It checks installed packages' registry signatures and verifies build records when present. A pass alone does not prove every package has provenance or that it came from this repository. Each platform package holds the binary from the release archive, checked against `checksums.txt` before packing. Official releases require provenance for the parent and all six platform packages. The release workflow also checks the GitHub Actions signing identity, this repository's release workflow at the version tag, the signed source commit, and the downloaded tarball integrity. It rejects missing or invalid signed build records, including for already published versions. These checks prove origin and integrity, not that the code has no defects. From a checkout of the release tag, using the release's npm CLI version (11.5.1), check any one package without installing or running it: ```bash . scripts/npm-publish.sh verify_npm_provenance agnostic-ai X.Y.Z verify_npm_provenance @agnostic-ai/linux-x64 X.Y.Z ``` If the registry is temporarily unavailable, retry later. A package version published without its signed build record cannot be repaired in place; maintainers must release a new version of all seven packages. ### Verify signed tags and commits ## Signed tags and commits Release commits and `vX.Y.Z` tags are GPG-signed with the maintainer's key, published at [github.com/Chemaclass.gpg](https://github.com/Chemaclass.gpg): ```bash curl -fsSL https://github.com/Chemaclass.gpg | gpg --import git verify-tag vX.Y.Z ``` ### Build the tagged source yourself ## Build from source `go install github.com/chemaclass/agnostic-ai/cmd/agnostic-ai@vX.Y.Z` builds the tagged source on your machine. Go checks the module against the [checksum database](https://sum.golang.org/), so a tag moved after publication fails. ## Report a problem A failed check on a published release is a security issue. Report it through a [private advisory](https://github.com/Chemaclass/agnostic-ai/security/advisories/new), not a public issue. --- # Why agnostic-ai? {#why-agnostic-ai} Each coding tool reads its instructions and settings from its own files. With several tools, you keep several copies current. agnostic-ai lets you edit one source and generates the files each tool reads. ## A small useful setup Start with a short project note in `.agnostic-ai/AGNOSTIC_AI.md`: ```markdown Application code is in src/. Tests are in tests/. Read CONTRIBUTING.md for the test command. Run the relevant tests before finishing a change. ``` Run `agnostic-ai sync`. Claude Code gets `CLAUDE.md`, Codex gets `AGENTS.md`, and other tools get their own files. Change the note and sync again to update them all. ## What agnostic-ai does Your source files live in `.agnostic-ai/`, in plain Markdown and YAML. They are called **specs**. The coding tools you use are called **targets**; you choose them in `agnostic-ai.yaml`. Sync translates the specs into each tool's paths, formats, and setting names, and reports features a tool cannot use. `agnostic-ai sync --check` tells you whether the generated files still match the specs. You still install and sign in to the coding tools yourself. Instructions guide the model; tests and tool permissions enforce checks and limits. A connection to an outside service still needs that service's login or key. ## What to add, and when Start with the project note. Add another kind of spec only when you need it: | You need | Use | Example | |---|---|---| | Project instructions | `.agnostic-ai/AGNOSTIC_AI.md` | Where code lives and how to test it | | A coding convention | A [rule](https://agnostic-ai.org/docs/spec-format/rules/) | Use the project's commit format | | Steps for a repeatable task | A [skill](https://agnostic-ai.org/docs/spec-format/skills/) | Prepare a release | | A specialist to delegate to | An [agent](https://agnostic-ai.org/docs/spec-format/agents/) | Review a change for defects | | Limits on what an agent may read, run, or call | Agent [capabilities](https://agnostic-ai.org/docs/spec-format/agents/#capabilities) | Let a reviewer read but not edit | | A command at a tool event | A [hook](https://agnostic-ai.org/docs/spec-format/hooks/) | Format a file after an edit | | Tool settings | [Settings](https://agnostic-ai.org/docs/spec-format/settings/) | Choose a model or permissions | | One model choice for every tool | [Model roles](https://agnostic-ai.org/docs/configuration/#models) | Use a fast model for lookups | | An outside service or data source | An [MCP connection](https://agnostic-ai.org/docs/spec-format/mcps/) | Read an issue tracker | | Guidance for code review bots | [Reviews](https://agnostic-ai.org/docs/spec-format/reviews/) | Flag missing tests | | Facts every tool can recall | [Shared memory](https://agnostic-ai.org/docs/memory/) | A decision and its reason | | Work you pick up in another tool | [Session handoffs](https://agnostic-ai.org/docs/handoff/) | One private note per branch | | Shorter command output and replies | [RTK and Caveman](https://agnostic-ai.org/docs/rtk-and-caveman/) | Fewer tokens per session | MCP (Model Context Protocol) is how coding tools connect to other tools and data. An MCP spec names the environment variables that hold credentials, so keys stay out of the repository. Each tool supports a different set of features; the [capability matrix](https://agnostic-ai.org/docs/targets/#capability-matrix) shows what each one gets. ## When you do not need it For one tool and a small setup, start with that tool's own files. Use agnostic-ai when you share a setup across tools or keep several formats current. If you stop using agnostic-ai, the generated files keep working; commit them first if Git ignores them. ### Why copies, symlinks, and one AGENTS.md have limits ## Why the usual fixes fall short Copies need separate updates. A symlink shares text but cannot translate formats or setting names. One `AGENTS.md` shares instructions with tools that read it, but cannot configure their native skills, hooks, or service connections. A custom script can translate the formats you maintain yourself. ## The same server, four formats One connection in `.agnostic-ai/mcps/github.yaml`: ```yaml name: github type: http url: https://api.githubcopilot.com/mcp/ ``` Sync writes the same connection in each tool's format: | Tool | File | Connection setting | |---|---|---| | Claude Code | `.mcp.json` | `mcpServers.github.url` | | Codex | `.codex/config.toml` | `[mcp_servers.github]` with `url` | | Gemini CLI | `.gemini/settings.json` | `mcpServers.github.httpUrl` | | Zed | `.zed/settings.json` | `context_servers.github.url` | ## Start [Install agnostic-ai](https://agnostic-ai.org/docs/installation/), then [set up your project](https://agnostic-ai.org/docs/getting-started/). Already have tool files? [Import them first](https://agnostic-ai.org/docs/migration/).