Matching Behavior
This page explains how runok parses commands and matches them against patterns.
Patterns are Parsed as Written
Section titled “Patterns are Parsed as Written”runok does not rewrite or preprocess patterns. The way you write a rule is exactly how it is parsed and matched:
- No implicit splitting or joining. Tokens are separated by spaces, and
=-joined values stay as a single token. - Rules are self-contained. You can understand a rule’s behavior by reading it alone —
definitionsdo not change how a pattern is parsed.
# "-Denv=prod" is a single token — matched as-is- deny: 'java -Denv=prod *'# Matches: java -Denv=prod -jar app.jar# Does NOT match: java -Denv staging -jar app.jar
# "-X" and "POST" are separate tokens — matched as flag and value- deny: 'curl -X POST *'# Matches: curl -X POST https://example.comFlag Schema Inference
Section titled “Flag Schema Inference”When a pattern contains a flag followed by a value, runok infers that the flag takes a value argument. This inference is used when parsing the actual command to correctly associate values with their flags.
# Pattern: curl -X|--request POST *# Inferred flag schema: -X and --request take a value- deny: 'curl -X|--request POST *'With this inferred schema, the command curl -X POST https://example.com is parsed as:
-X— flagPOST— value of-Xhttps://example.com— positional argument
Without this inference, POST would be treated as a positional argument rather than a flag value.
Flags in Optional Groups
Section titled “Flags in Optional Groups”Flags inside optional groups are also included in the inferred schema:
# Both -o/--output and -X/--request are inferred as value flags- allow: 'curl [-o|--output *] -X|--request GET *'Order-independent Flag Matching
Section titled “Order-independent Flag Matching”Flags (tokens starting with -) in patterns are matched regardless of their position in the command:
- allow: 'git push -f|--force *'| Command | Result |
|---|---|
git push --force origin main | Matches |
git push origin --force main | Matches |
git push origin main --force | Matches |
This applies to standalone flags (alternation), flag-value pairs, and flag-only negations. The matcher scans the entire command token list to find a matching flag, removes it, and continues matching the remaining tokens.
=-joined Flag Values
Section titled “=-joined Flag Values”Flag-value patterns also match =-joined forms. A pattern like -X POST matches both the space-separated curl -X POST and the =-joined curl -X=POST:
- deny: 'curl -X|--request POST *'| Command | Result |
|---|---|
curl -X POST https://example.com | Matches |
curl -X=POST https://example.com | Matches |
curl --request=POST https://example.com | Matches |
Standalone flag alternations (without an explicit value pattern) also recognize =-joined tokens. The flag part is matched against the alternation and the value part becomes a separate token for subsequent pattern matching:
- ask: 'curl * -o|--output *'- allow: 'curl *'| Command | Result |
|---|---|
curl --output /tmp/out https://example.com | ask |
curl --output=/tmp/out https://example.com | ask |
curl -o=/tmp/out https://example.com | ask |
curl https://example.com | allow |
Fused Short Flag Values
Section titled “Fused Short Flag Values”Flag-value patterns also match fused short flags, where the value is directly attached to the flag character without a space or =. A pattern like -n * matches -n 3 (space-separated), -n=3 (=-joined), and -n3 (fused):
- allow: 'git tag [-n *] *'| Command | Result |
|---|---|
git tag -n 3 v1 | Matches |
git tag -n=3 v1 | Matches |
git tag -n3 v1 | Matches |
git tag v1 | Matches |
Fused splitting only applies to short flags (single - followed by a single ASCII character). It is only attempted when the pattern declares a FlagWithValue for that flag (e.g. -n *), so combined boolean flags like -rf are not falsely split.
Flag-only Negation
Section titled “Flag-only Negation”Negation patterns where all alternatives start with - also use order-independent matching. The matcher scans the entire command for any token matching the negated pattern and rejects the match if found. Unlike positional negation, flag-only negation does not consume a positional token — it only asserts that the forbidden flag is absent. This means it also passes when there are no command tokens (the flag is trivially absent):
- allow: 'find !-delete|-fprint|-fls *'| Command | Result |
|---|---|
find . -name foo -type f | Matches |
find | Matches |
find . -delete | Does not match |
find -fprint output . | Does not match |
This also works with =-joined flags. For example, !--pre rejects both --pre value (space-separated) and --pre=value (=-joined):
- allow: 'rg !--pre *'| Command | Result |
|---|---|
rg pattern file.txt | Matches |
rg --pre pdftotext pat | Does not match |
rg --pre=pdftotext pat | Does not match |
Positional Arguments Skip Over Flags
Section titled “Positional Arguments Skip Over Flags”Non-flag positional tokens — both literals and alternations — also benefit from order-independent matching. When matching a positional token, the matcher skips over any leading flag tokens in the command to find the first positional argument. This means flags can appear before positional arguments without breaking the match:
- allow: 'gh api -X GET *'| Command | Result |
|---|---|
gh api -X GET /repos | Matches |
gh -X GET api /repos | Matches |
gh api /repos -X GET | Matches |
Positional arguments are still matched in order relative to each other:
- allow: 'git push origin main'| Command | Result |
|---|---|
git push origin main | Matches |
git push main origin | Does not match |
The bare -- separator is treated as a positional token, not a flag. It is always matched at its exact position to preserve the distinction between arguments before and after --.
Backslash Escapes
Section titled “Backslash Escapes”A backslash (\) in a pattern escapes the following character. During matching, the backslash is stripped and the remaining character is compared literally. This is useful for characters that have special meaning in shells, such as ;:
# \; in the pattern matches ; in the command- "find * -exec <cmd> \\;|+"The shell resolves \; to ; before runok sees the command, so the pattern’s \; (after unescape) matches the command’s ;.
Combined Short Flags
Section titled “Combined Short Flags”Combined short flags like -am are not split into individual flags — they are matched as a single token, exactly as written:
- deny: 'git commit -m *'| Command | Result | Reason |
|---|---|---|
git commit -m "fix bug" | Matches | -m matches directly |
git commit -am "fix bug" | Does not match | -am is a different token than -m |
If you want to match -am, write it explicitly:
- deny: 'git commit -am *'Recursion Limit
Section titled “Recursion Limit”To prevent pathological patterns (such as many consecutive wildcards) from causing excessive computation, matching is limited to 10,000 steps. Patterns that exceed this limit fail to match.
Related
Section titled “Related”- Pattern Syntax Overview — All syntax elements at a glance.
- Architecture: Pattern Matching — Internal implementation details.