Skip to content

Placeholders

Tokens wrapped in <...> are placeholders — special tokens that match dynamically rather than by exact string comparison.

PlaceholderDescription
<cmd>Captures the wrapped command for further rule evaluation
<opts>Absorbs zero or more flag-like tokens
<vars>Absorbs zero or more KEY=VALUE tokens
<path:name>Matches against a named list of paths
<var:name>Matches against a typed variable definition
<flag:name>Matches and captures every occurrence of any flag in a named flag alias group

The <cmd> placeholder captures the remaining tokens as the wrapped command. The wrapped command is then evaluated against the other rules in the configuration. See Wrapped Command Recursion for details.

<cmd> only matches token sequences whose first token is not a flag (does not start with -). This prevents wrapper patterns from accidentally consuming flag arguments as commands. For example, command <cmd> does not match command -v a because -v is a flag, not a command name.

# sudo echo hello -> wrapped command is "echo hello"
- allow: 'sudo <cmd>'

Wrapper patterns are defined in the definitions.wrappers block and referenced by rules:

definitions:
wrappers:
- 'sudo <cmd>'
- 'xargs <opts> <cmd>'
rules:
- allow: 'echo *'
# With the wrapper definition, this also allows:
# sudo echo hello
# xargs -I{} echo hello

The <opts> placeholder absorbs zero or more flag-like tokens (tokens starting with -):

# Matches: env FOO=bar command
# Matches: env -i FOO=bar command
# Matches: env -u HOME -i FOO=bar command
- allow: 'env <opts> <vars> <cmd>'

<opts> stops consuming tokens when it encounters:

  • A token that does not start with -
  • The -- end-of-options marker

For short flags consisting of exactly - plus one ASCII letter (e.g., -n, -S), if the next token does not start with -, it is consumed as the flag’s argument:

# env -S "FOO=bar" command -> <opts> consumes "-S" and "FOO=bar"
- allow: 'env <opts> <cmd>'

The <vars> placeholder absorbs zero or more KEY=VALUE tokens — tokens that contain =:

# Matches: env command
# Matches: env FOO=bar command
# Matches: env FOO=bar BAZ=qux command
- allow: 'env <vars> <cmd>'

<vars> stops consuming tokens when it encounters a token without =.

The <path:name> placeholder matches a command argument against a named list of paths defined in the definitions.paths block.

definitions:
paths:
sensitive:
- /etc/passwd
- /etc/shadow
- /etc/sudoers
config:
- /etc/nginx/nginx.conf
- /etc/hosts

Reference a path list with <path:name>:

rules:
- deny: 'cat <path:sensitive>'
- deny: 'rm <path:sensitive>'
- allow: 'cat <path:config>'
CommandRuleResult
cat /etc/passwddeny: "cat <path:sensitive>"Denied
cat /etc/hostsallow: "cat <path:config>"Allowed
rm /etc/shadowdeny: "rm <path:sensitive>"Denied

Paths are normalized before comparison. The following path components are resolved:

  • . (current directory) is removed
  • .. (parent directory) is resolved
definitions:
paths:
sensitive:
- /etc/passwd
CommandMatches <path:sensitive>
cat /etc/passwdYes
cat /etc/./passwdYes (. removed)
cat /tmp/../etc/passwdYes (.. resolved)

This prevents bypassing path rules through path manipulation.

If a pattern references a path name that is not defined in definitions.paths, the pattern never matches:

# If "sensitive" is not defined, this rule has no effect
- deny: 'cat <path:sensitive>'

The <var:name> placeholder matches a command token against a typed variable definition in the definitions.vars block. It can be used in both argument position and command position.

Each variable has an optional type (default: literal) and a list of values. Values can be plain strings (inheriting the definition-level type) or objects with an explicit per-value type override:

definitions:
vars:
instance-ids:
values:
- i-abc123
- i-def456
- i-ghi789
test-script:
type: path
values:
- ./tests/run
runok:
values:
- runok # literal (default): exact match
- 'cargo run --' # literal: multi-word, consumes multiple tokens
- type: path
value: target/debug/runok # path: canonicalize before comparison
TypeMatching behavior
literalExact string match (default)
pathCanonicalize both sides before comparison, fallback to path normalization

Each value inherits the definition-level type unless it specifies its own type via the { type, value } form.

rules:
- allow: aws ec2 terminate-instances --instance-ids <var:instance-ids>
- allow: bash <var:test-script>
CommandRuleResult
aws ec2 terminate-instances --instance-ids i-abc123allow: "... --instance-ids <var:instance-ids>"Allowed
aws ec2 terminate-instances --instance-ids i-UNKNOWNallow: "... --instance-ids <var:instance-ids>"No match
bash ./tests/runallow: "bash <var:test-script>"Allowed
bash tests/runallow: "bash <var:test-script>"Allowed (path normalization)

<var:name> can also be used as the command name in a pattern. This is useful when the same tool can be invoked in multiple ways:

definitions:
vars:
runok:
values:
- runok
- 'cargo run --'
- type: path
value: target/debug/runok
rules:
- allow: '<var:runok> check'
CommandResult
runok checkAllowed
cargo run -- checkAllowed (multi-word value)
./target/debug/runok checkAllowed (path normalization)
node checkNo match

Multi-word values (e.g. "cargo run --") consume multiple leading tokens from the input command.

When type: path is set, both the command argument and the defined values are canonicalized (resolved to absolute paths via the filesystem). If the path does not exist on disk, logical normalization is used as a fallback (. removal and .. resolution).

This handles cases where the same file is referenced with different path forms:

definitions:
vars:
test-script:
type: path
values:
- ./tests/run
CommandMatches <var:test-script>
bash tests/runYes
bash ./tests/runYes
bash ./tests/../tests/runYes
bash ./scripts/deployNo

If a pattern references a variable name that is not defined in definitions.vars, the pattern never matches.

The <flag:name> placeholder matches any flag that belongs to a named flag alias group defined in definitions.flag_groups. It is purpose-built for two common needs:

  1. Treating flag aliases uniformly. Many CLIs accept the same flag under several spellings — gh api exposes -f, -F, --field, and --raw-field; curl accepts -d, --data, --data-raw, etc. With a single flag-group definition, you can match every alias with one placeholder.
  2. Inspecting every value of a repeatable flag. Repeatable flags (-d for curl, -v for docker, -f for gh api graphql) take multiple values per invocation. The <flag:name> placeholder collects every captured value into a list, exposed to when clauses via flag_groups[name].
runok.yml
definitions:
flag_groups:
field-flag: ['-f', '-F', '--field', '--raw-field']
header-flag: ['-H', '--header']

<flag:name> is always followed by a value pattern (a wildcard or literal). The value pattern is matched against the value of every captured flag:

runok.yml
rules:
# Allow any gh api graphql call where every -f/-F/--field/--raw-field
# value is a query (not a mutation).
- allow: 'gh api graphql <flag:field-flag> *'
when: '!flag_groups["field-flag"].exists(v, v.startsWith("query=mutation"))'
- ask: 'gh api graphql <flag:field-flag> *'
Commandflag_groups["field-flag"]
gh api graphql -f query=query{...}["query=query{...}"]
gh api graphql --raw-field query=query{...}["query=query{...}"]
gh api graphql --raw-field=query=query{...}["query=query{...}"]
gh api graphql -f query=query{...} -f variables={}["query=query{...}", "variables={}"]
gh api graphql -F query=mutation{...}["query=mutation{...}"]
  • The pattern matches only when at least one of the group’s aliases appears in the command (mirroring how a -f|--field|--raw-field VALUE alternation behaves today).
  • Every space-separated (-f value), =-joined (-f=value or --field=value), and fused short-flag (-fvalue) form is recognized.
  • Each captured value is also validated against the value pattern; if any captured value fails to match, the whole rule does not apply.

You could write -f|-F|--field|--raw-field VALUE as a flag alternation, but two limitations push you toward <flag:name>:

  • The alternation only matches the first occurrence of any alias (the rest are silently kept around). For repeatable flags this loses information.
  • The captured value is exposed to when clauses through flags, where each spelling (-f vs --field) lives under a different key, making list-aware checks like exists(v, ...) impossible.

<flag:name> solves both problems: it captures every occurrence and surfaces the values as a single list under flag_groups[name].

Referencing a flag group that is not defined in definitions.flag_groups is a validation error at config load time. Unlike <path:name> and <var:name> (which silently fail to match), undefined flag groups always indicate a typo or missing definition, so they are reported eagerly.

Placeholders can be combined to handle complex wrapper patterns:

definitions:
wrappers:
# Handles: env [-i] [-u NAME] [KEY=VALUE...] command [args...]
- 'env <opts> <vars> <cmd>'
# Handles: sudo [-u user] command [args...]
- 'sudo <opts> <cmd>'
# Handles: xargs [flags...] command [args...]
- 'xargs <opts> <cmd>'
# Handles: find [args...] -exec|-execdir|-ok|-okdir command [args...] \;|+
- "find * -exec|-execdir|-ok|-okdir <cmd> \\;|+"
  • <cmd> captures one or more tokens whose first token is not a flag (does not start with -); it tries all possible split points to find a valid wrapped command
  • Optional groups, path references, and variable references are not supported inside wrapper patterns