Type to search the docs and updates.

Browse documentation

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, deny rm, 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

FieldRequiredDefaultDescription
permissions.allownoemptyRules approved without prompting.
permissions.denynoemptyRules always blocked.
permissions.asknoemptyRules that prompt before running.
permissions.default-modenounsetClaude Code starting mode for sync --global: default, manual, acceptEdits, plan, auto, dontAsk, or bypassPermissions. Other targets raise a coverage note.
modelnoemptyDefault 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.
effortnoemptyDefault reasoning effort: a scalar, or a map per target with an optional default, like agent effort. Separate from an agent's own effort.
protectednounsetFiles 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.
FieldRequiredDefaultDescription
pathsyesGlobs 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.
decisionnoaskask makes the agent ask first. deny blocks the edit.
reasonnoemptyShown 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 lint ignore case, so .GITHUB/ci.yml counts as protected on a case-insensitive file system. Claude Code's permission docs do not say whether Edit rules ignore case.
TargetProtectionHow
Claude Codeenforced (permission)Edit(/<path>) rules in permissions.ask or permissions.deny (details)
Codexenforced (hook)a generated PreToolUse hook in .codex/hooks/ that blocks a matching apply_patch (details)
Cursorenforced (permission), CLI only, deny onlyWrite(<path>) rules in permissions.deny of .cursor/cli.json; ask stays advisory (details)
Gemini CLIenforced (hook)a generated BeforeTool hook in .gemini/hooks/ that blocks a matching write_file or replace (details)
Every other targetadvisorya 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 lint warns (LINT022) when a protected path covers a file sync writes, because sync regenerates that file from its source spec.
  • lint reports an invalid block as LINT023.
  • sync --global does 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.

RuleCoversClaude Code rule
read, read(<path>)Reading files, or the files that matchRead, Read(<path>)
edit, edit(<path>)Changing files, or the files that matchEdit, Edit(<path>)
writeThe write toolWrite
deleteDeleting files, where the target has a delete toolNo native tool
shell, shell(<pattern>)Every command, or the commands that matchBash, Bash(<pattern>)
webFetching pages and searching the webWebFetch and WebSearch
mcp:<server>, mcp:<server>/<tool>One MCP server, or one of its toolsmcp__<server>, mcp__<server>__<tool>
  • Each rule maps to the target's native permission names. Existing Claude Code aliases keep the same mappings. delete raises 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.
  • write takes no path. Claude Code checks file writes against Edit rules only and never consults a Write(<path>) rule, so write edit(<path>) to cover a file.
  • 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 target would drop it.
  • agnostic-ai migrate --only capabilities rewrites each Claude Code rule a capability stands for alone. An adjacent WebFetch, WebSearch pair becomes web. 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 a deny rule 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>.

TargetNative keyAccepted values
Claude CodeeffortLevel in .claude/settings.jsonlow, medium, high, xhigh
CopiloteffortLevel in .github/copilot/settings.jsonlow, medium, high, xhigh
Codexmodel_reasoning_effort in .codex/config.tomlany string; outputs.codex.config.model-reasoning-effort and the captured overlay win
FactoryreasoningEffort in .factory/settings.jsonnone, dynamic, off, minimal, low, medium, high, xhigh, max; each model accepts a subset

Support by target

Targetpermissionsmodel
Claude Code, Qoder, Kilo Code, OpenCodeyesyes
FactoryBash rules onlyyes
CodexBash rules via outputs.codex.exec-policies-from-permissionsyes
Windsurfyesno
Augmentallow and deny onlyno
Cursorallow and deny in .cursor/cli.json, for the Cursor CLI (details)no
Copilot, Junie, Gemininoyes

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: true turns 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, and write as whole tools. Factory's command lists take shell patterns. A path-scoped rule raises a note instead of widening.
  • Factory's commandDenylist prompts, so portable ask goes there and deny goes to commandBlocklist.
  • Review an imported model before you enable more targets, because identifiers differ between vendors.
  • A shared Claude model name raises the same coverage note as an agent model on Codex, Gemini, OpenCode, Kilo Code, and Factory. Codex skips it when outputs.codex.config.model or 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 model is 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 the x-<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).