Skip to content

Priority Model: Explicit Deny Wins

When multiple rules match a single command, runok must decide which action to take. runok uses the Explicit Deny Wins model, inspired by AWS IAM policy evaluation: a deny rule always overrides allow and ask, regardless of rule order.

Each action has a fixed restriction level. When multiple rules match, the most restrictive action wins:

PriorityActionMeaning
2 (highest)denyBlock the command
1askPrompt the user for confirmation
0allowPermit the command

This priority is defined in action_priority() in the rule engine (src/rules/rule_engine.rs).

  1. runok iterates over all rules and collects every rule whose pattern matches the input command (and whose when clause, if any, evaluates to true).
  2. Among all matching rules, the one with the highest restriction level determines the final action.
  3. Rule order in the config file does not affect priority. A deny rule always wins, even if an allow rule appears later.
rules:
- allow: 'git *' # priority 0
- deny: 'git push -f|--force *' # priority 2 — always wins

In this example, git push --force main matches both rules. The deny (priority 2) overrides the allow (priority 0), so the command is blocked.

AspectAWS IAMrunok
Explicit denyAlways winsAlways wins
AllowPermits unless deniedPermits unless denied or asked
DefaultImplicit deny (block)Configurable via defaults.action
Ask (confirmation)Not applicableMiddle tier between allow and deny

The key difference is that runok adds an ask tier between allow and deny, and the default action is configurable rather than fixed to deny.

When no rule matches a command, the action is resolved immediately to the configured defaults.action:

defaults:
action: ask # "allow", "deny", or "ask"

If defaults.action is not set, it defaults to ask.

Because unmatched commands are resolved at evaluation time, they participate directly in the Explicit Deny Wins comparison at their effective restriction level. For example, during compound command evaluation, an unmatched sub-command resolved to ask (priority 1) will correctly outrank an allow (priority 0) sub-command.

When a command matches a definitions.wrappers pattern (e.g., sudo <cmd>), the wrapped command is extracted and evaluated recursively. The result from the wrapped command evaluation is then merged with any direct rule matches using the same Explicit Deny Wins logic.

For example:

definitions:
wrappers:
- 'sudo <cmd>'
rules:
- allow: 'sudo *'
- deny: 'rm -rf /'

When evaluating sudo rm -rf /:

  1. allow: "sudo *" matches directly (priority 0).
  2. sudo <cmd> extracts the wrapped command rm -rf / and evaluates it recursively.
  3. deny: "rm -rf /" matches the wrapped command (priority 2).
  4. The results are merged: deny (priority 2) wins over allow (priority 0).

The command is denied.

For a single command, the sandbox preset is taken from whichever rule wins the Explicit Deny Wins comparison. The sandbox preset does not affect the priority comparison itself — it is carried along with the winning action.

rules:
- allow: 'npm install *'
sandbox: restricted-network

For compound commands, sandbox presets from all sub-commands are merged using a strictest intersection strategy.