Permissions
[permissions] is one allow / ask / deny list for every harness that has a native permission file. You write Claude
Code permission rules once in .ai-rulez/config.toml, and ai-rulez generate translates them into each harness’s
own file and format. A rule a harness cannot express exactly is skipped with a warning, never widened.
presets = ["claude", "codex", "cursor"]
[permissions]allow = ["Bash(git status)", "Bash(npm run test:*)"]ask = ["Bash(git push:*)"]deny = ["Read(./.env)", "Bash(rm -rf:*)"]generate writes the rules verbatim for Claude Code:
{ "permissions": { "allow": [ "Bash(git status)", "Bash(npm run test:*)" ], "ask": [ "Bash(git push:*)" ], "deny": [ "Read(./.env)", "Bash(rm -rf:*)" ] }}Codex rules match command prefixes, so the exact-command allow and the path deny are left out:
# Generated by ai-rulez from [permissions]. Do not edit; change .ai-rulez/config.toml and regenerate....prefix_rule( pattern = ["npm","run","test"], decision = "allow",)
prefix_rule( pattern = ["git","push"], decision = "prompt",)
prefix_rule( pattern = ["rm","-rf"], decision = "forbidden",)Cursor’s Shell() rules take one command name, so neither allow fits, and Cursor has no ask list:
{ "permissions": { "allow": [], "deny": [ "Read(.env)", "Shell(rm:-rf*)" ] }}Every skipped rule is reported:
WARN [permissions] allow rule "Bash(git status)" not generated for codex: Codex rules match a token prefix, so an exact-command allow would be widenedWARN SECURITY: [permissions] deny rule "Read(./.env)" is NOT enforced by codex: Codex has no rule surface for Read (paths, domains and MCP tools need a permission profile, which replaces sandbox_mode) severity=error hint=enforce it another way (sandbox, hook) or remove the harness from the projectWARN [permissions] ask rules are not generated for cursor: it has no ask list and prompts for whatever is not allowedWARN [permissions] allow rule "Bash(git status)" not generated for cursor: Cursor's Shell() rules take a command name; a multi-word, exact or wildcard allow cannot be expressed without widening itWARN [permissions] allow rule "Bash(npm run test:*)" not generated for cursor: Cursor's Shell() rules take a command name; a multi-word, exact or wildcard allow cannot be expressed without widening itRun ai-rulez generate --dry-run to see the warnings without writing files.
Rule syntax
Section titled “Rule syntax”A rule is Tool or Tool(specifier), as in Claude Code:
| Rule | Meaning |
|---|---|
Bash, Read, Edit, WebFetch |
every call of the tool |
Bash(npm run test:*) |
a command and any arguments; :* is the legacy form of npm run test * |
Bash(git status) |
exactly that command |
Bash(git * --force) |
a command glob |
Read(./.env), Edit(src/**), Write(dist/**) |
a path glob: ./x or x is relative to the working directory, /x to the project root, //x is absolute, ~/x is in the home directory |
WebFetch(domain:example.com) |
a host |
mcp__github__create_issue, mcp__github |
an MCP tool, or every tool of a server |
Task(Explore) |
a subagent |
Every translator uses the same parser, so a rule means the same thing everywhere. A rule that does not parse is skipped with a warning.
validate reports an allow rule that permits every call of a tool (Bash, Bash(*), *, WebFetch) as AR506 permission-overbroad. Deny and ask lists are not checked. ai-rulez does not evaluate rules; the harness does (Claude
Code applies deny over ask over allow).
What a translation guarantees
Section titled “What a translation guarantees”- A rule is never widened. When a harness cannot express an allow exactly (it matches command prefixes where you
wrote an exact command, one
editpermission covers every edit tool,?is a wildcard where Claude Code treats it as a literal), the allow is skipped with a warning. - A deny is never lost silently. A deny a harness cannot enforce is reported as
SECURITY: [permissions] deny rule "..." is NOT enforced by <harness>: <why>. A preset with no permission file at all is named in one warning. Enforce such rules another way (a sandbox, a hook) or leave the harness out. - Ask rules are written where the harness has an ask list. Where it prompts for everything that is not allowed, ask rules are not written and one warning says so.
- Your own rules survive. Array entries and map members are owned one by one, and comments in JSONC, TOML and YAML
stay. Removing a rule from
config.tomlremoves exactly that entry on the nextgenerate;cleantakes back whatgeneratewrote. A rule you wrote that equals a configured one stays yours onclean. A setting you already have with another value is kept, with a warning, so a rule of yours is never relaxed. - Project-wide only. A
[[scopes]]subdirectory renders no permissions.
Harnesses
Section titled “Harnesses”“Rules” lists what each harness can express; everything else is skipped as described above.
| Harness | File | Format | Allow / ask / deny | Rules |
|---|---|---|---|---|
claude |
.claude/settings.json |
JSON | yes / yes / yes | all (verbatim) |
codex |
.codex/rules/ai-rulez.rules |
Starlark | yes / prompt / forbidden |
command prefixes only |
opencode |
opencode.json |
JSONC | yes / yes / yes | commands, read, edit, webfetch, websearch, task |
kilo |
kilo.jsonc |
JSONC | yes / yes / yes | as opencode |
mimocode |
.mimocode/mimocode.jsonc |
JSONC | yes / yes / yes | as opencode |
gemini |
.gemini/settings.json |
JSON | yes / no / yes | command prefixes; whole tools |
cursor |
.cursor/cli.json |
JSON | yes / no / yes | commands, read, write, webfetch, mcp |
copilot |
.vscode/settings.json |
JSONC | yes / yes / prompt only | commands, edits, urls |
copilot-cli |
.github/copilot/settings.json |
JSONC | yes / no / yes | URLs only |
zoocode |
.vscode/settings.json |
JSONC | yes / no / yes | command prefixes |
devin |
.devin/config.json |
JSONC | yes / yes / yes | commands, read, write, fetch, mcp |
qwen |
.qwen/settings.json |
JSON | yes / yes / yes | Claude syntax |
codebuddy |
.codebuddy/settings.json |
JSON | yes / yes / yes | Claude syntax |
commandcode |
.commandcode/settings.json |
JSON | yes / yes / yes | Claude syntax (Shell) |
qoder |
.qoder/settings.json |
JSON | yes / yes / yes | Claude syntax, no WebFetch |
letta |
.letta/settings.json |
JSON | yes / alwaysAsk / yes |
Bash, Read, Edit |
grok |
.grok/config.toml |
TOML | yes / yes / yes | commands, read, edit, mcp, whole web tools |
vibe |
.vibe/config.toml |
TOML | yes / no / yes | command prefixes; deny of read and edit paths; whole-tool deny |
poolside |
.poolside/settings.yaml |
YAML | yes / no / yes | commands, paths |
omp |
.omp/config.yml |
YAML | yes / yes / yes | commands; whole tools |
augment |
.augment/settings.json |
JSONC | yes / no / yes | commands; whole tools |
zed |
~/.config/zed/settings.json (--user only) |
JSONC | yes / yes / yes | commands, edit (deny/ask), fetch, mcp tools |
hermes |
~/.hermes/config.yaml (--user only) |
YAML | yes / no / yes | commands |
kimi |
~/.kimi-code/config.toml (--user only) |
TOML | yes / yes / yes | Bash, bare Read |
User-level files
Section titled “User-level files”generate --user writes the user-level counterpart of every harness above that has one:
~/.claude/settings.json, ~/.codex/rules/ai-rulez.rules, ~/.gemini/settings.json,
~/.config/opencode/opencode.json, ~/.config/kilo/kilo.jsonc, ~/.config/mimocode/mimocode.jsonc,
~/.copilot/settings.json (copilot-cli), ~/.qwen/settings.json, ~/.codebuddy/settings.json,
~/.commandcode/settings.json, ~/.letta/settings.json, ~/.grok/config.toml, ~/.vibe/config.toml,
~/.config/poolside/settings.yaml, ~/.omp/agent/config.yml, ~/.augment/settings.json, ~/.qoder/settings.json,
and the permissions key of ~/.config/devin/config.json. cursor, copilot and zoocode have a project file only.
zed, hermes and kimi read permissions from the user file only, so a project generate writes nothing for them
and warns once.
Per-harness details
Section titled “Per-harness details”- codex (rules, marked experimental by Codex):
prefix_rule(pattern=[argv tokens], decision=...); the most restrictive match wins. Paths, domains and MCP tools need a permission profile (default_permissions), which cannot be combined withsandbox_mode, so those rules are not generated. ai-rulez owns the whole rules file; put your own rules in another*.rulesfile. - opencode, kilo, mimocode (OpenCode permissions): the last matching rule
wins, so a rule that would come after an overlapping, stricter one is skipped rather than allowed to relax it.
Bashprefixes becomecmd *; aWebFetchdomain becomeshttps://host/*(a deny also covershttpand the bare host). MCP tool names are not a permission key, so MCP rules are skipped. - gemini (configuration):
tools.allowedandtools.excludeofsettings.json, which match command prefixes and whole tools, never a path or a domain. Project policy files (.gemini/policies/*.toml) are documented as non-functional, so they are not used. - cursor (permissions):
Shell(git)covers everygitcommand, so only single-word prefixes can be allowed; a multi-word deny usesShell(git:push*). There is no ask list.permissions.allowandpermissions.denyare both required, so a deny-only config writes"allow": []. - copilot (approvals):
trueauto-approves andfalsealways asks. VS Code has no hard deny, so a deny is written asfalseand reported as not enforced. Read and MCP rules have no setting.copilotandzoocodeshare.vscode/settings.json. - zoocode: the longest matching prefix wins and a tie goes to deny, so an allow longer than a deny prefix of it is skipped. Command auto-approval also needs the extension’s auto-approve toggle, which is not a settings value.
- copilot-cli (config directory):
tool permissions are session flags; only
allowedUrlsanddeniedUrlscan be committed. - devin (permissions).
- qwen, codebuddy, commandcode, qoder, letta: Claude Code syntax under
permissions.{allow,ask,deny}. Command Code calls the shell toolShell. Command Code and Qoder match a bare path at any depth, so paths are written project-rooted (/src/**). Qoder’s Edit rule covers Write, so a Write allow is skipped. Letta’sasklist does not override a broader allow, so ask rules go toalwaysAsk. - grok (permissions): the project config holds
[permission]. The WebFetch domain syntax is not documented, so only a bareWebFetchrule is written. - vibe: setting
allowlistordenylistreplaces Vibe’s shipped default for that list. Path denies are written as*/<glob>because Vibe matches the absolute path; a path allow cannot be scoped, so it is skipped. - poolside (tool permissions): the project file takes project-relative paths only.
- omp:
bash.patternsis first-match-wins; ai-rulez appends deny, ask, then allow after your own patterns, so a pattern of yours that matches first still wins. There is no per-path or per-domain matcher. - augment (permissions): only allow and deny; a command rule is a
shellInputRegex; no path or domain matcher. - zed (tool permissions):
agent.tool_permissionsis read from the user settings only. Read tools are not permission-gated in Zed, so read rules are skipped (useprivate_files). - hermes, kimi (Hermes security, Kimi config files): user-level files only.
Not translated
Section titled “Not translated”These harnesses have no documented, committable permission file that can hold the rules, so they get nothing from
[permissions] and generate names them in a warning:
| Harness | Why |
|---|---|
amp |
Amp has no permission rules; only whole-tool amp.tools.disable |
pi |
Pi has no permission mechanism; defaultTools only enables or disables whole tools |
cline |
Command permissions exist only as the CLINE_COMMAND_PERMISSIONS environment variable, not a file |
kiro |
The permissions in .kiro/agents/*.json are documented with conflicting shapes, and the workspace permissions file lives outside the repository |
factory |
The project settings path is documented as settings.local.json, and commandDenylist means “ask”, not “deny” |
crush |
crush.json has allowed_tools (whole tools) and no deny |
rovodev |
The location of the bash rules and their match order are not documented consistently |
takt |
Only a coarse permission mode (readonly, edit, full), no rules |
antigravity |
Only the user-level CLI file is documented, not a project path |
goose, junie, deepagents, warp |
User-level files with whole-tool, allow-only, exact-command or regex semantics that cannot hold the rules without widening them |
The other presets without a permission row (aiassistant, baz, bob, codebuff, …) have no permission surface.
Matching semantics that change a rule
Section titled “Matching semantics that change a rule”Where a harness matches differently from Claude Code, ai-rulez adjusts the rule so it means the same or less:
- Whole-command regular expressions (
augment,zed). The harness tests one regex against the command string, so^git log(\s|$)would also matchgit log; rm -rf /. An allow is written as^git log([ \t][^;&|\n\r$()<>]*)?$: the command, then arguments free of separators, substitution, grouping and redirections (a command that uses them is not auto-approved). A deny is written broader: Augment's also matches the command after a separator, a subshell or a backtick, and Zed's stays anchored because Zed tests every chained sub-command. A deny cannot see through a wrapper (env git push,sh -c ‘git push’`): enforce important denies with a sandbox as well. - Bare prefixes (
zoocode,gemini,copilot). Zoo Code and Gemini match with a plain prefix test, sogitwould approvegitk: allow prefixes are written with a trailing space (git). For VS Code an allow is written as the anchored regex/^git status(\s|$)/. Denies stay plain prefixes, since a broader deny only blocks more. - Paths (
opencode,kilo,mimocode). Patterns are matched against the path relative to the worktree, and*matches/too. An allow with a single*(Read(./src/*.go)) or?would widen, and a home or absolute path allow would never apply, so these are skipped. A deny is written for the root and for any depth (.envand**/.env); a home or absolute deny is written as given and reported asSECURITY: ... may not be enforced. - Domains.
WebFetch(domain:Host.COM)is lowercased. OpenCode-family denies also coverhttps://host:*andhttp://host:*, becausehttps://host/*does not match a URL with a port.
Lossy cases at a glance
Section titled “Lossy cases at a glance”- A command glob (
Bash(git * --force)) is skipped wherever the harness matches prefixes only (codex,gemini,cursor,devin,vibe,zoocode). - A path or domain rule is skipped wherever the harness approves whole tools only (
gemini,omp,augment,vibeallow,codex). - A skipped allow means a prompt where you wanted none; a skipped deny is reported as
SECURITY: ....
See also
Section titled “See also”- Hooks and settings: hooks, the generated-file guard and file ownership.
- Supported harnesses: what every preset writes.
