Skip to Content

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 substring

Rules 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 match

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

ToolArg matched
execute_commandcommand
write_file, read_file, open_filepath
download_file, grep, glob, list_dirpath
connect, ssh_session, cmdophostname
ask_agent, ask_agent_streamhostname

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:

PatternMatchesDoesn’t match
git *git status, git push origin maingita, mygit
~/projects/**~/projects/a.txt, ~/projects/sub/x~/Projects/a.txt (Linux)
prod-*prod-1, prod-east-2prodserver, prod
internal-*.example.cominternal-db.example.comdb.example.com

Actions

  • allow — execute, audited as allow_rule.
  • deny — refuse, audited as deny_rule.
  • ask — wait for a UI decision, audited as ask_allowed / ask_denied / ask_timeout.

deny always wins over ask over allow when multiple rules match. Floor wins over everything.

Scopes

  • global — written to permissions.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:56Z
Last updated on