Wrapped Command Recursion
Commands like sudo, bash -c, and xargs wrap another command inside them. runok recognizes these patterns and recursively evaluates the wrapped command, so rules apply to what actually runs, not just the outer command.
Defining wrapped command patterns
Section titled “Defining wrapped command patterns”Wrapped command patterns are defined in the definitions.wrappers section of your config. The <cmd> placeholder marks where the wrapped command appears:
definitions: wrappers: - 'bash -c <cmd>' - 'sudo <cmd>' - 'xargs <opts> <cmd>' - 'env <opts> <vars> <cmd>'The <cmd> placeholder captures one or more tokens. For single-token captures (like bash -c "ls -la"), the captured string is passed as-is for further parsing. For multi-token captures (like sudo ls -la), the tokens are re-quoted using shell quoting rules before evaluation.
For full details on <cmd> and other placeholders, see Placeholders.
How recursion works
Section titled “How recursion works”When evaluate_command() processes a command, it tries two things in parallel:
- Direct rule matching: Check all rules against the command.
- Wrapped command extraction: Check if the command matches any
definitions.wrapperspattern and extract the wrapped command via<cmd>.
If both produce a result, they are merged using Explicit Deny Wins.
Step-by-step example
Section titled “Step-by-step example”Config:
definitions: wrappers: - 'sudo <cmd>' - 'bash -c <cmd>'
rules: - allow: 'sudo *' - allow: 'ls *' - deny: 'rm -rf /'Command: sudo bash -c "rm -rf /"
Step 1: Depth 0 — Evaluate sudo bash -c "rm -rf /"
- Direct rules:
allow: "sudo *"matches →allow sudo <cmd>matches → wrapped command:bash -c "rm -rf /"- Recurse into wrapped command (depth 1)
Step 2: Depth 1 — Evaluate bash -c "rm -rf /"
- Direct rules: no match → resolved to
defaults.action bash -c <cmd>matches → wrapped command:rm -rf /- Recurse into wrapped command (depth 2)
Step 3: Depth 2 — Evaluate rm -rf /
- Direct rules:
deny: "rm -rf /"matches →deny - No
<cmd>pattern matches - Result: deny
Unwinding:
- Depth 1: merge
defaults.actionwithdeny→ deny wins - Depth 0: merge
allowwithdeny→ deny wins
Final result: deny. The dangerous wrapped command is blocked even though sudo * is allowed.
Compound commands inside wrapped commands
Section titled “Compound commands inside wrapped commands”When the wrapped command extracted via <cmd> is a compound command (containing |, &&, ||, or ;), it is split into individual sub-commands using tree-sitter-bash. Each sub-command is evaluated separately, and results are merged using Explicit Deny Wins.
definitions: wrappers: - 'bash -c <cmd>'
rules: - allow: 'ls *' - deny: 'rm -rf *'Command: bash -c "ls /tmp; rm -rf /"
bash -c <cmd>extracts wrapped command:ls /tmp; rm -rf /- Compound command is split into:
ls /tmpandrm -rf / ls /tmp→allowrm -rf /→deny- Merged result: deny (strictest wins)
Recursion depth limit
Section titled “Recursion depth limit”To prevent infinite recursion (e.g., sudo sudo sudo ...), runok enforces a maximum recursion depth of 10. If this limit is exceeded, the evaluation returns a RecursionDepthExceeded error.
The depth counter increments each time a wrapped command is extracted and evaluated. The limit is defined as MAX_WRAPPER_DEPTH = 10 in src/rules/rule_engine.rs.
Ambiguous captures
Section titled “Ambiguous captures”When a wrapper pattern uses placeholders like <opts>, the placeholder consumes matching tokens (e.g., flag-like tokens starting with -) and the remaining tokens become the wrapped command via <cmd>. If a wildcard * is used instead of <opts>, multiple split points are possible, and runok tries all captures and selects the one with the highest action priority (most restrictive).
Default action resolution
Section titled “Default action resolution”When the wrapped command does not match any rule, its action is resolved immediately to the configured defaults.action (defaulting to ask if unconfigured). This ensures that unmatched wrapped commands participate in the merge at their effective restriction level, rather than being silently ignored.