Settings
settings/ sets what agents may run without asking, what they must never run, and which model and effort they start with. Every tool spells this differently: Claude Code permissions in .claude/settings.json, Factory command lists, a Codex config.toml. A settings spec states the policy once.
- One policy. Allow
go test, denyrm, and ask before pushing, in every tool that supports permissions. - Safe translation. A rule a tool cannot express raises a coverage note instead of being widened.
- Protected files. List the files an agent must not edit without asking. Each tool gets its own guard for them.
- One default model. Set it per tool with a fallback, since model names differ between vendors.
- Tool-only keys too. An
x-<target>block merges into that tool's own settings file.
Write one
Settings specs are pure YAML, one file per settings group, such as settings/permissions.yaml.
permissions:
allow:
- read(src/**)
- shell(go test:*)
deny:
- shell(rm:*)
- edit(.env)
ask:
- shell(git push:*)
model: claude-opus-4-8
effort:
claude: xhigh
default: high
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 starting mode for sync --global: default, manual, acceptEdits, plan, auto, dontAsk, or bypassPermissions. Other targets raise a coverage note. |
model | no | empty | Default model: a string, or a map per target with an optional default, like agent model, or a tier name. A target with no entry and no default gets no model. |
effort | no | empty | Default reasoning effort: a scalar, or a map per target with an optional default, like agent effort. Separate from an agent's own effort. |
protected | no | unset | Files agents must not edit without asking: paths, decision (ask or deny), and reason. See protected paths. |
Protected paths
"Do not edit these files without asking" is a common project rule for CI workflows, lock files, generated code, and migrations. A protected block states it once. Sync writes the strongest native form each target has.
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 relative to the project root. * and ? match inside one path segment. ** matches across segments. A path also covers every file under it, so 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 an edit is blocked. |
A path is anchored at the project root. composer.lock protects only the root file. **/composer.lock protects every copy.
- Character classes, braces, negation, and paths outside the project are rejected, because the targets would read them differently.
- Each settings spec holds one block, so use one file per decision.
- The Codex and Gemini CLI hooks and
lintignore case, so.GITHUB/ci.ymlcounts as protected on a case-insensitive file system. Claude Code's permission docs do not say whetherEditrules ignore case.
| Target | Protection | How |
|---|---|---|
| Claude Code | enforced (permission) | Edit(/<path>) rules in permissions.ask or permissions.deny (details) |
| Codex | enforced (hook) | a generated PreToolUse hook in .codex/hooks/ that blocks a matching apply_patch (details) |
| Cursor | enforced (permission), CLI only, deny only | Write(<path>) rules in permissions.deny of .cursor/cli.json; ask stays advisory (details) |
| Gemini CLI | enforced (hook) | a generated BeforeTool hook in .gemini/hooks/ that blocks a matching write_file or replace (details) |
| Every other target | advisory | a coverage note on sync; state the paths in a rule |
Protection covers the agent's edit tools. A shell command or script that writes the file directly can still change it.
agnostic-ai lintwarns (LINT022) when a protected path covers a file sync writes, because sync regenerates that file from its source spec.lintreports an invalid block as LINT023.sync --globaldoes not write protected paths.
Permission rules
A rule is a capability, the same names an agent's can: takes. In a permission list, read and edit also take a path pattern.
| Rule | Covers | Claude Code rule |
|---|---|---|
read, read(<path>) | Reading files, or the files that match | Read, Read(<path>) |
edit, edit(<path>) | Changing files, or the files that match | Edit, Edit(<path>) |
write | The write tool | Write |
delete | Deleting files, where the target has a delete tool | No native tool |
shell, shell(<pattern>) | Every command, or the commands that match | Bash, Bash(<pattern>) |
web | Fetching pages and searching the web | WebFetch and WebSearch |
mcp:<server>, mcp:<server>/<tool> | One MCP server, or one of its tools | mcp__<server>, mcp__<server>__<tool> |
- Each rule maps to the target's native permission names. Existing Claude Code aliases keep the same mappings.
deleteraises a coverage note where a target has no native delete permission. - A Claude Code rule stays valid as an alias, such as
WebFetch(domain:go.dev). A list can mix both. writetakes no path. Claude Code checks file writes againstEditrules only and never consults aWrite(<path>)rule, so writeedit(<path>)to cover a file.validate,lint(LINT036), andsyncstop on a rule they cannot read, including one in a pack and an unquoted rule that YAML reads as a mapping, since a target would drop it.agnostic-ai migrate --only capabilitiesrewrites each Claude Code rule a capability stands for alone. An adjacentWebFetch, WebSearchpair becomesweb. The rest stay as aliases, and sync writes the same files.
Bare capabilities under allow or ask cover the whole native tool. Before permission capabilities, these lowercase rules matched no tool and did nothing. lint warns (LINT038) and sync prints the same note once each run, naming the native permissions on enabled targets that take the rule. Scoped rules, deny rules, and target-native overrides that replace the rule get no warning.
Use shell(git status), read(src/**), edit(src/**), or mcp:github/get_issue to limit the rule. edit(<path>) also covers writes and edits. web has no scoped neutral form; use target-native permission fields to limit web access, or the WebFetch(domain:example.com) alias to allow fetches from a named domain.
Set on-unsupported: silent or pass sync --quiet to hide this sync note. lint still reports LINT038, and lint --strict fails on it. Global permission lists have no native allow or ask rule and raise no LINT038.
A Claude Code rule is a bare tool name (whole tool) or Scope(argument). An MCP tool is mcp__<server>__<tool>. Scope() with an empty argument is dropped, not read as the bare tool, which would widen it.
Keep a shell wildcard at the end of an allow or deny rule.
shell(git * main)also approves options inserted at the*.- Claude Code matches a mid-command
*in adenyrule literally, so it blocks nothing.
agnostic-ai lint reports both as LINT009.
explain settings/<name>.yaml lists the native rules each configured target gets. It names any extra access the translation grants. on-unsupported: error fails when a rule widens, including a Codex exact shell allow that becomes a command prefix.
Merging
Multiple files merge. Permission lists concatenate, de-duplicated in source order. The last non-empty model and effort win. Each target resolves its own map entry first, 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).
sync --global also reads settings specs from the home, for model, effort, target-specific keys, and permissions.default-mode. See default model and effort.
Effort by target
effort reaches four targets, each under its own key. A value the target does not accept is not written and raises a coverage note. The other targets still emit. x-<target> wins, so x-claude.effortLevel overrides the portable value. Every other target reports a coverage note.
import fills effort from these keys when the target accepts the value and no other settings spec sets it. Otherwise the value lands under x-<target>.
| 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 |
| Factory | reasoningEffort in .factory/settings.json | none, dynamic, off, minimal, low, medium, high, xhigh, max; each model accepts a subset |
Support by target
| Target | permissions | model |
|---|---|---|
| Claude Code, Qoder, Kilo Code, OpenCode | yes | yes |
| Factory | Bash rules only | yes |
| Codex | Bash rules via outputs.codex.exec-policies-from-permissions | yes |
| Windsurf | yes | no |
| Augment | allow and deny only | no |
| Cursor | allow and deny in .cursor/cli.json, for the Cursor CLI (details) | no |
| Copilot, Junie, Gemini | no | yes |
Every other target takes neither. A field a target cannot represent produces a coverage note while the others still emit.
- Copilot and Junie report the whole policy.
- Codex does too, unless
outputs.codex.exec-policies-from-permissions: trueturns simple Bash rules into exec policies. The rest raise a note (Bash permission translation). - Rules translate only as far as the vendor allows. Augment gates
read,edit, andwriteas whole tools. Factory's command lists take shell patterns. A path-scoped rule raises a note instead of widening. - Factory's
commandDenylistprompts, so portableaskgoes there anddenygoes tocommandBlocklist. - Review an imported
modelbefore you enable more targets, because identifiers differ between vendors. - A shared Claude model name raises the same coverage note as an agent
modelon Codex, Gemini, OpenCode, Kilo Code, and Factory. Codex skips it whenoutputs.codex.config.modelor the captured overlay sets the model.
Target-specific keys
Target-specific keys go under x-<target> and merge into that target's settings file (x-factory.sandbox reaches .factory/settings.json). Codex is the exception. Its .codex/config.toml comes from the captured overlay plus outputs.codex.config, so an x-codex block raises a coverage note that names both routes.
On a key this tool also writes:
- Lists union, with translated entries first.
- Objects merge recursively.
- A scalar such as
modelis replaced. - A list against a string cannot merge. The
x-<target>value wins, with a coverage note. - Maps of whole records (
x-qoder.mcpServers,x-augment.mcpServers) merge by name. A server both sides name comes from thex-<target>block entire (#974). - Fixed-order blocks (
x-qoder.hooks,x-augment.hooks) keep their order, with your own events appended (#976).
Four keys take one shape each: x-augment.toolPermissions a list, and x-windsurf.permissions, x-kilo.permission, and x-opencode.permission an object. Another shape is skipped, the translated rules are still written, and a coverage note names the key (#976).