Rule grammar
Permission rules use a small, gitignore-flavored grammar. This page is the reference; for the broader model see Concepts: permissions.
Grammar
rule = tool ( "(" body ")" )?
body = argGlob # non-dispatcher tools
| operation # dispatcher tools
| operation ":" argGlob # dispatcher tools
tool = lowercase_with_underscores
operation = identifier
argGlob = gitignore pattern, '*' matches any substringRules are stored as YAML in permissions.yaml (mode 0600, atomic write) or in-memory for session scope.
Examples
execute_command(git *) # allow any git command
execute_command(rm *) # deny all rm
write_file(.env*) # also blocked by floor; gate sees it second
write_file(~/projects/**) # path glob anywhere under projects
read_file(/var/log/**) # tail logs but not /etc
connect(exec:prod-*) # dispatcher: exec on hostname glob
ssh_session(open:internal-*) # open persistent sessions only on internal-*
ask_agent(prod-1) # exact hostname matchQuote rules in the shell — your shell will eat the parens otherwise:
cmdop permissions allow 'execute_command(git *)'Tool to argument table
The matcher knows which argument of each tool to compare against argGlob:
| Tool | Arg matched |
|---|---|
execute_command | command |
write_file, read_file, open_file | path |
download_file, grep, glob, list_dir | path |
connect, ssh_session, cmdop | hostname |
ask_agent, ask_agent_stream | hostname |
Source: internal/agent/permissions/matcher.go::argKeyForTool.
Glob semantics
*— match any substring within a single segment (path component or command word).**— match any depth (zero or more segments).- Patterns anchor on the full argument value; partial matches fail.
- Case-sensitive on Linux, case-insensitive on macOS / Windows for filesystem paths.
Examples:
| Pattern | Matches | Doesn’t match |
|---|---|---|
git * | git status, git push origin main | gita, mygit |
~/projects/** | ~/projects/a.txt, ~/projects/sub/x | ~/Projects/a.txt (Linux) |
prod-* | prod-1, prod-east-2 | prodserver, prod |
internal-*.example.com | internal-db.example.com | db.example.com |
Actions
allow— execute, audited asallow_rule.deny— refuse, audited asdeny_rule.ask— wait for a UI decision, audited asask_allowed/ask_denied/ask_timeout.
deny always wins over ask over allow when multiple rules match. Floor wins over everything.
Scopes
global— written topermissions.yaml(mode 0600), survives daemon restarts.session— in-memory for the current daemon process. Useful for one-off “let me run this for the next hour” without committing to disk.
CLI:
cmdop permissions allow --scope=session 'execute_command(npm test)'Reasons
Optional human-readable explanation, shown in audit:
cmdop permissions allow --reason 'CI runs git commands' 'execute_command(git *)'The reason lands in the YAML and the audit log, helpful for code review of permissions.yaml checked into a repo.
YAML shape
version: 1
mode: default
bypass_token: "" # optional, sanitized in show_policy output
allow:
- rule: execute_command(git *)
reason: developer convenience
deny:
- rule: write_file(.env*)
reason: floor would deny anyway, explicit for clarity
ask:
- rule: write_file(~/projects/**)
created_at: 2026-04-25T12:34:56ZRelated
Last updated on