Skip to content

Matching Behavior

This page explains how runok parses commands and matches them against patterns.

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 — definitions do 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.com

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 — flag
  • POST — value of -X
  • https://example.com — positional argument

Without this inference, POST would be treated as a positional argument rather than a flag value.

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 *'

Flags (tokens starting with -) in patterns are matched regardless of their position in the command:

- allow: 'git push -f|--force *'
CommandResult
git push --force origin mainMatches
git push origin --force mainMatches
git push origin main --forceMatches

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.

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 *'
CommandResult
curl -X POST https://example.comMatches
curl -X=POST https://example.comMatches
curl --request=POST https://example.comMatches

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 *'
CommandResult
curl --output /tmp/out https://example.comask
curl --output=/tmp/out https://example.comask
curl -o=/tmp/out https://example.comask
curl https://example.comallow

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 *] *'
CommandResult
git tag -n 3 v1Matches
git tag -n=3 v1Matches
git tag -n3 v1Matches
git tag v1Matches

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.

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 *'
CommandResult
find . -name foo -type fMatches
findMatches
find . -deleteDoes 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 *'
CommandResult
rg pattern file.txtMatches
rg --pre pdftotext patDoes not match
rg --pre=pdftotext patDoes not match

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 *'
CommandResult
gh api -X GET /reposMatches
gh -X GET api /reposMatches
gh api /repos -X GETMatches

Positional arguments are still matched in order relative to each other:

- allow: 'git push origin main'
CommandResult
git push origin mainMatches
git push main originDoes 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 --.

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 like -am are not split into individual flags — they are matched as a single token, exactly as written:

- deny: 'git commit -m *'
CommandResultReason
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 *'

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.