Skip to content

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.

.ai-rulez/config.toml
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:

.claude/settings.json
{
"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:

.codex/rules/ai-rulez.rules
# 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:

.cursor/cli.json
{
"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 widened
WARN 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 project
WARN [permissions] ask rules are not generated for cursor: it has no ask list and prompts for whatever is not allowed
WARN [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 it
WARN [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 it

Run ai-rulez generate --dry-run to see the warnings without writing files.

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

  • A rule is never widened. When a harness cannot express an allow exactly (it matches command prefixes where you wrote an exact command, one edit permission 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.toml removes exactly that entry on the next generate; clean takes back what generate wrote. A rule you wrote that equals a configured one stays yours on clean. 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.

“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

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.

  • 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 with sandbox_mode, so those rules are not generated. ai-rulez owns the whole rules file; put your own rules in another *.rules file.
  • 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. Bash prefixes become cmd *; a WebFetch domain becomes https://host/* (a deny also covers http and the bare host). MCP tool names are not a permission key, so MCP rules are skipped.
  • gemini (configuration): tools.allowed and tools.exclude of settings.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 every git command, so only single-word prefixes can be allowed; a multi-word deny uses Shell(git:push*). There is no ask list. permissions.allow and permissions.deny are both required, so a deny-only config writes "allow": [].
  • copilot (approvals): true auto-approves and false always asks. VS Code has no hard deny, so a deny is written as false and reported as not enforced. Read and MCP rules have no setting. copilot and zoocode share .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 allowedUrls and deniedUrls can be committed.
  • devin (permissions).
  • qwen, codebuddy, commandcode, qoder, letta: Claude Code syntax under permissions.{allow,ask,deny}. Command Code calls the shell tool Shell. 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’s ask list does not override a broader allow, so ask rules go to alwaysAsk.
  • grok (permissions): the project config holds [permission]. The WebFetch domain syntax is not documented, so only a bare WebFetch rule is written.
  • vibe: setting allowlist or denylist replaces 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.patterns is 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_permissions is read from the user settings only. Read tools are not permission-gated in Zed, so read rules are skipped (use private_files).
  • hermes, kimi (Hermes security, Kimi config files): user-level files only.

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.

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 match git 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, so git would approve gitk: 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 (.env and **/.env); a home or absolute deny is written as given and reported as SECURITY: ... may not be enforced.
  • Domains. WebFetch(domain:Host.COM) is lowercased. OpenCode-family denies also cover https://host:* and http://host:*, because https://host/* does not match a URL with a port.
  • 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, vibe allow, codex).
  • A skipped allow means a prompt where you wanted none; a skipped deny is reported as SECURITY: ....