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. 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:

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 first. Other tools are listed below.

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

FieldRequiredDefaultDescription
permissions.allownoemptyRules approved without prompting.
permissions.denynoemptyRules always blocked.
permissions.asknoemptyRules that prompt before running.
permissions.default-modenounsetClaude Code's starting mode for sync --global: default, manual, acceptEdits, plan, auto, dontAsk, or bypassPermissions. Other tools raise a sync note.
modelnoemptyDefault model: a string, 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 single value, or a map per target with an optional default. An agent's own effort is separate. See effort by target.
session-retention-daysnounsetPersonal session age limit, an integer of 1 or more. Only sync --global writes it. See session retention.
voicenounsetPersonal voice dictation: enabled, mode (hold or tap), and language. Only sync --global writes it. See voice dictation.
protectednounsetFiles agents must not edit without asking. See protected paths.

Session retention

Keep this personal setting under ~/.agnostic-ai/settings/, outside team project specs:

# ~/.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.

ToolUser settings fileNative key
Claude Code~/.claude/settings.jsoncleanupPeriodDays: 3650
CodeBuddy Code~/.codebuddy/settings.jsoncleanupPeriodDays: 3650
Gemini CLI~/.gemini/settings.jsongeneral.sessionRetention.maxAge: "3650d"
Qoder CLI~/.qoder/settings.jsongeneral.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, Gemini configuration, and Qoder settings.

Voice dictation

Keep this personal setting under ~/.agnostic-ai/settings/, outside team project specs:

# ~/.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.

ToolUser settings fileNative keys
Claude Code~/.claude/settings.jsonvoice.enabled, voice.mode
Qwen Code~/.qwen/settings.jsongeneral.voice.enabled, general.voice.mode, general.voice.language
Gemini CLI~/.gemini/settings.jsonexperimental.voiceMode, experimental.voice.activationMode (hold becomes push-to-talk, tap becomes toggle)
Grok Build~/.grok/config.tomlui.voice_capture_mode (tap becomes toggle), ui.voice_stt_language
Mistral Vibe~/.vibe/config.tomlvoice_mode_enabled
Kiro CLI~/.kiro/settings/cli.jsonLiteral root key voice.language
WarpPlatform settings fileagents.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-<target> 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, Qwen settings, Gemini's voice schema, Grok settings, and 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 uses each tool's strongest available guard.

protected:
  paths:
    - .github/**
    - composer.lock
  decision: ask     # ask (default) or deny
  reason: CI and the lock file change only on purpose.
FieldRequiredDefaultDescription
pathsyesGlobs 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.
decisionnoaskask makes the agent ask first. deny blocks the edit.
reasonnoemptyShown 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.
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 is not enforced (details)
Gemini CLIenforced (hook)a generated BeforeTool hook in .gemini/hooks/ that blocks a matching write_file or replace (details)
Every other targetnot enforceda 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 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(<path>) 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/<name>.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).

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.

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-<target> 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-<target>. Kimi import does not read effort.

TargetNative keyAccepted values
AugmentdefaultThinkingLevel in .augment/settings.json, read by Auggie 1.xoff, minimal, low, medium, high, xhigh
Claude CodeeffortLevel in .claude/settings.jsonlow, medium, high, xhigh
CopiloteffortLevel in .github/copilot/settings.jsonnone, minimal, low, medium, high, xhigh, max; each model accepts a subset
Codexmodel_reasoning_effort in .codex/config.tomlany string; outputs.codex.config.model-reasoning-effort and the captured overlay win
CodeBuddyreasoningEffort in .codebuddy/settings.jsonminimal, low, medium, high, xhigh, max
FactoryreasoningEffort in .factory/settings.jsonnone, dynamic, off, minimal, low, medium, high, xhigh, max; each model accepts a subset
Junieeffort in .junie/config.jsonany string
Kimi[thinking].effort in the config.toml that outputs.kimi.conf-file namesany value
PidefaultThinkingLevel in .pi/settings.jsonoff, minimal, low, medium, high, xhigh, max

Support by target

Targetpermissionsmodel
Claude Code, Qoder, Kilo Code, OpenCodeyesyes
FactoryBash rules only; ask goes to commandDenylist (which prompts), deny to commandBlocklistyes
CodexBash rules via outputs.codex.exec-policies-from-permissions (translation); otherwise a noteyes
Windsurfyesno
Augmentallow, ask, and deny, for whole tools and Bash prefixes; path rules raise a notedefaultModel, read by Auggie 1.x
Cursorallow and deny in .cursor/cli.json, for the Cursor CLI (details)no
Copilot, Junie, Gemini, Kirono; Copilot, Junie, and Kiro report the whole policy in a noteyes

Other targets support neither field. Unsupported fields raise a sync note; output for other tools continues. 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 on Codex, Gemini, OpenCode, Kilo Code, Factory, and Kiro; Codex skips it when outputs.codex.config.model or the captured overlay 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-<target> 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-<target> 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-<target> 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/<name>.yaml. A later spec replaces each top-level block it sets (details).
  • 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.

Edit scalar settings in the browser

Run agnostic-ai ui in a project, or agnostic-ai ui --global 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.<tool> or effort.<tool>. 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). A browser save does not sync: run agnostic-ai sync or agnostic-ai sync --global afterward.